2026-07-13 15:29:29 +08:00
import pg from 'pg' ;
import {
buildControlSchemaSql ,
2026-07-17 09:32:31 +08:00
buildEnsureCurrentUserCanSetRoleSql ,
2026-07-13 15:29:29 +08:00
deriveUserSpaceNames ,
provisionUserSpace ,
quotePgIdentifier ,
} from './mindspace-userdata-postgres.mjs' ;
const pools = new Map ( ) ;
const provisionedUsers = new Set ( ) ;
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/ ;
function ident ( value , label = '标识符' ) {
const text = String ( value ? ? '' ) . trim ( ) ;
if ( ! IDENTIFIER . test ( text ) ) throw Object . assign ( new Error ( ` ${ label } 格式无效 ` ) , { code : 'invalid_identifier' } ) ;
return text ;
}
function normalizeDataset ( raw , fallbackName = null ) {
const value = typeof raw === 'string' ? JSON . parse ( raw ) : raw ;
if ( ! value || typeof value !== 'object' ) throw Object . assign ( new Error ( 'dataset 配置无效' ) , { code : 'invalid_dataset_config' } ) ;
const name = ident ( value . name ? ? fallbackName , 'dataset 名称' ) ;
const table = ident ( value . table ? ? value . table _name , 'dataset 表名' ) ;
const columns = value . columns && typeof value . columns === 'object' ? value . columns : { } ;
for ( const [ action , fields ] of Object . entries ( columns ) ) {
if ( ! Array . isArray ( fields ) ) throw Object . assign ( new Error ( ` dataset 字段白名单无效: ${ action } ` ) , { code : 'invalid_dataset_config' } ) ;
columns [ action ] = fields . map ( ( field ) => ident ( field , ` ${ action } 字段 ` ) ) ;
}
return {
name ,
table ,
description : String ( value . description ? ? '' ) ,
actions : Array . isArray ( value . actions ) ? value . actions . map ( String ) : [ ] ,
columns ,
limits : {
maxRowsPerRead : Number ( value . limits ? . maxRowsPerRead ? ? 200 ) ,
maxInsertBytes : Number ( value . limits ? . maxInsertBytes ? ? 8192 ) ,
} ,
} ;
}
function poolConfig ( options ) {
if ( options . connectionString ? ? process . env . MINDSPACE _USERDATA _PG _URL ) {
return { connectionString : options . connectionString ? ? process . env . MINDSPACE _USERDATA _PG _URL , max : 5 } ;
}
return {
host : options . pgHost ? ? process . env . MINDSPACE _USERDATA _PG _HOST ? ? '/tmp' ,
port : Number ( options . pgPort ? ? process . env . MINDSPACE _USERDATA _PG _PORT ? ? 5433 ) ,
database : options . pgDatabase ? ? process . env . MINDSPACE _USERDATA _PG _DATABASE ? ? 'mindspace_userdata_dev' ,
user : options . pgUser ? ? process . env . MINDSPACE _USERDATA _PG _USER ? ? process . env . USER ,
max : 5 ,
} ;
}
function sharedPool ( options ) {
if ( options . pgPool ) return options . pgPool ;
const config = poolConfig ( options ) ;
const key = JSON . stringify ( config ) ;
if ( ! pools . has ( key ) ) pools . set ( key , new pg . Pool ( config ) ) ;
return pools . get ( key ) ;
}
function rejectSql ( sql , { readonly = false } = { } ) {
const cleaned = String ( sql ? ? '' ) . replace ( /--.*$/gm , '' ) . replace ( /\/\*[\s\S]*?\*\//g , '' ) . trim ( ) ;
if ( ! cleaned || cleaned . length > 20000 ) throw new Error ( 'SQL 为空或过长' ) ;
if ( readonly && ! /^(SELECT|WITH)\b/i . test ( cleaned ) ) throw new Error ( 'private_data_query 只允许 SELECT/WITH' ) ;
if ( /\b(CREATE|ALTER|DROP)\s+(ROLE|USER|DATABASE|TABLESPACE|EXTENSION)|ALTER\s+SYSTEM|COPY[\s\S]+PROGRAM|SECURITY\s+DEFINER|pg_(read|write)_file|lo_import|dblink/i . test ( cleaned ) ) {
throw new Error ( 'SQL 包含用户空间不允许的操作' ) ;
}
if ( /\bSET\s+(ROLE|SESSION_AUTHORIZATION|search_path)\b/i . test ( cleaned ) ) throw new Error ( 'SQL 不允许改变安全上下文' ) ;
return cleaned ;
}
function translateSqliteAgentDdl ( sql ) {
return sql
. replace ( /INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/gi , 'BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY' )
. replace ( /TEXT\s+NOT\s+NULL\s+DEFAULT\s*\(datetime\(\s*'now'\s*,\s*'(?:\+8 hours|localtime)'\s*\)\)/gi , 'TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP' )
. replace ( /TEXT\s+DEFAULT\s*\(datetime\(\s*'now'\s*,\s*'(?:\+8 hours|localtime)'\s*\)\)/gi , 'TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP' ) ;
}
export function createPostgresUserDataSpaceService ( options = { } ) {
const names = deriveUserSpaceNames ( options . userId ) ;
const pool = sharedPool ( options ) ;
const schema = quotePgIdentifier ( names . schemaName ) ;
const role = quotePgIdentifier ( names . agentRole ) ;
const maxRows = Number ( options . maxRows ? ? 200 ) ;
async function ensureProvisioned ( { force = false } = { } ) {
if ( provisionedUsers . has ( names . userId ) ) return ;
const client = await pool . connect ( ) ;
try {
await client . query ( buildControlSchemaSql ( ) ) ;
const existing = await client . query ( 'SELECT migration_state FROM mindspace_control.user_spaces WHERE user_id=$1::uuid' , [ names . userId ] ) ;
if ( ! existing . rows [ 0 ] ) {
if ( ! force && String ( options . autoProvision ? ? process . env . MINDSPACE _USERDATA _AUTO _PROVISION ? ? '0' ) !== '1' ) {
throw Object . assign ( new Error ( '用户 PG 空间尚未分配' ) , { code : 'user_space_not_provisioned' } ) ;
}
await provisionUserSpace ( client , names . userId , { sourceSqlitePath : options . workspaceRoot ? ` ${ options . workspaceRoot } /.mindspace/private-data.sqlite ` : null } ) ;
await client . query ( 'BEGIN' ) ;
try {
await client . query ( ` CREATE TABLE IF NOT EXISTS ${ schema } .__page_data_datasets (name text PRIMARY KEY, table_name text NOT NULL, config_json text NOT NULL, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP) ` ) ;
await client . query ( ` ALTER TABLE ${ schema } .__page_data_datasets OWNER TO ${ quotePgIdentifier ( names . ownerRole ) } ` ) ;
await client . query ( ` GRANT SELECT,INSERT,UPDATE,DELETE ON ${ schema } .__page_data_datasets TO ${ role } ` ) ;
await client . query ( "UPDATE mindspace_control.user_spaces SET migration_state='cutover',cutover_at=CURRENT_TIMESTAMP,rollback_until=CURRENT_TIMESTAMP + INTERVAL '30 days',updated_at=CURRENT_TIMESTAMP WHERE user_id=$1::uuid" , [ names . userId ] ) ;
await client . query ( 'COMMIT' ) ;
} catch ( error ) {
await client . query ( 'ROLLBACK' ) ;
throw error ;
}
2026-07-17 09:32:31 +08:00
} else {
// PostgreSQL 17 grants CREATEROLE owners ADMIN but not SET by default.
// Reconcile existing spaces before the runtime executes SET LOCAL ROLE.
await client . query ( buildEnsureCurrentUserCanSetRoleSql ( names . agentRole ) ) ;
2026-07-13 15:29:29 +08:00
}
provisionedUsers . add ( names . userId ) ;
} finally {
client . release ( ) ;
}
}
async function tenant ( fn , { write = false } = { } ) {
await ensureProvisioned ( ) ;
const client = await pool . connect ( ) ;
let quotaBytes = null ;
try {
await client . query ( 'BEGIN' ) ;
if ( write ) {
const quota = await client . query ( 'SELECT quota_bytes FROM mindspace_control.user_spaces WHERE user_id=$1::uuid' , [ names . userId ] ) ;
quotaBytes = Number ( quota . rows [ 0 ] ? . quota _bytes ? ? 0 ) ;
}
await client . query ( ` SET LOCAL ROLE ${ role } ` ) ;
await client . query ( ` SET LOCAL search_path = ${ schema } , pg_catalog ` ) ;
await client . query ( ` SET LOCAL statement_timeout = ' ${ write ? 30 : 10 } s' ` ) ;
const result = await fn ( client ) ;
if ( write && quotaBytes > 0 ) {
const usage = await client . query (
` SELECT COALESCE(sum(pg_total_relation_size(c.oid)),0)::bigint AS bytes
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE n.nspname= $ 1 AND c.relkind IN ('r','p','m') ` ,
[ names . schemaName ] ,
) ;
if ( Number ( usage . rows [ 0 ] . bytes ) > quotaBytes ) {
throw Object . assign ( new Error ( ` 用户 PG 空间超过配额: ${ usage . rows [ 0 ] . bytes } / ${ quotaBytes } ` ) , { code : 'quota_exceeded' } ) ;
}
}
await client . query ( write ? 'COMMIT' : 'ROLLBACK' ) ;
if ( write ) {
await client . query (
` INSERT INTO mindspace_control.audit_events(user_id,actor_type,actor_id,action,result,detail_json)
VALUES( $ 1::uuid,'agent', $ 2,'postgres_write','success', $ 3::jsonb) ` ,
[ names . userId , names . agentRole , JSON . stringify ( { schema : names . schemaName } ) ] ,
) ;
}
return result ;
} catch ( error ) {
await client . query ( 'ROLLBACK' ) . catch ( ( ) => { } ) ;
if ( write ) {
await client . query (
` INSERT INTO mindspace_control.audit_events(user_id,actor_type,actor_id,action,result,detail_json)
VALUES( $ 1::uuid,'agent', $ 2,'postgres_write','failed', $ 3::jsonb) ` ,
[ names . userId , names . agentRole , JSON . stringify ( { schema : names . schemaName , code : error ? . code ? ? null } ) ] ,
) . catch ( ( ) => { } ) ;
}
throw error ;
} finally {
client . release ( ) ;
}
}
async function listTableColumns ( tableName ) {
const table = ident ( tableName , '表名' ) ;
return tenant ( async ( client ) => {
const result = await client . query (
` SELECT column_name AS name, data_type AS type,
(is_nullable = 'NO') AS "notNull",
(SELECT EXISTS (SELECT 1 FROM pg_index i JOIN pg_attribute a ON a.attrelid=i.indrelid AND a.attnum=ANY(i.indkey) WHERE i.indrelid=( $ 1||'.'|| $ 2)::regclass AND i.indisprimary AND a.attname=c.column_name)) AS pk
FROM information_schema.columns c WHERE table_schema= $ 1 AND table_name= $ 2 ORDER BY ordinal_position ` ,
[ names . schemaName , table ] ,
) ;
return result . rows ;
} ) ;
}
async function getSchema ( ) {
return tenant ( async ( client ) => ( await client . query (
` SELECT table_name, column_name, data_type AS type, (is_nullable='NO') AS not_null
FROM information_schema.columns WHERE table_schema= $ 1 ORDER BY table_name,ordinal_position ` ,
[ names . schemaName ] ,
) ) . rows ) ;
}
async function querySql ( sql ) {
const cleaned = rejectSql ( sql , { readonly : true } ) . replace ( /;\s*$/ , '' ) ;
return tenant ( async ( client ) => ( await client . query ( ` SELECT * FROM ( ${ cleaned } ) AS q LIMIT ${ maxRows } ` ) ) . rows ) ;
}
async function executeSql ( sql ) {
const cleaned = translateSqliteAgentDdl ( rejectSql ( sql ) ) ;
return tenant ( async ( client ) => {
const result = await client . query ( cleaned ) ;
return { backend : 'postgres' , command : result . command , affectedRows : result . rowCount ? ? 0 , sizeBytes : null , deltaBytes : null , quotaSynced : false } ;
} , { write : true } ) ;
}
async function getDataset ( name ) {
const datasetName = ident ( name , 'dataset 名称' ) ;
return tenant ( async ( client ) => {
const result = await client . query ( 'SELECT name,table_name,config_json,created_at,updated_at FROM __page_data_datasets WHERE name=$1 LIMIT 1' , [ datasetName ] ) ;
if ( ! result . rows [ 0 ] ) return null ;
return { ... normalizeDataset ( result . rows [ 0 ] . config _json , result . rows [ 0 ] . name ) , createdAt : result . rows [ 0 ] . created _at , updatedAt : result . rows [ 0 ] . updated _at } ;
} ) ;
}
async function listDatasets ( ) {
return tenant ( async ( client ) => ( await client . query ( 'SELECT name,table_name,config_json,created_at,updated_at FROM __page_data_datasets ORDER BY name' ) ) . rows . map ( ( row ) => ( {
... normalizeDataset ( row . config _json , row . name ) , createdAt : row . created _at , updatedAt : row . updated _at ,
} ) ) ) ;
}
async function upsertDataset ( input ) {
const dataset = normalizeDataset ( input ) ;
if ( ! ( await listTableColumns ( dataset . table ) ) . length ) throw Object . assign ( new Error ( ` dataset 对应表不存在: ${ dataset . table } ` ) , { code : 'table_not_found' } ) ;
await tenant ( ( client ) => client . query (
` INSERT INTO __page_data_datasets(name,table_name,config_json,updated_at) VALUES( $ 1, $ 2, $ 3,CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET table_name=EXCLUDED.table_name,config_json=EXCLUDED.config_json,updated_at=CURRENT_TIMESTAMP ` ,
[ dataset . name , dataset . table , JSON . stringify ( dataset ) ] ,
) , { write : true } ) ;
return getDataset ( dataset . name ) ;
}
function assertAction ( dataset , action ) {
if ( ! dataset . actions . includes ( action ) ) throw Object . assign ( new Error ( ` dataset 未授权 ${ action } ` ) , { code : 'action_not_allowed' } ) ;
}
async function readRowsForDataset ( datasetInput , options = { } ) {
const dataset = await Promise . resolve ( datasetInput ) ;
assertAction ( dataset , 'read' ) ;
const columns = dataset . columns . read ? . map ( ( item ) => ident ( item , '字段' ) ) ? ? [ ] ;
if ( ! columns . length ) throw Object . assign ( new Error ( 'dataset 未配置可读字段' ) , { code : 'columns_not_allowed' } ) ;
const limit = Math . min ( Math . max ( 1 , Number ( options . limit ? ? dataset . limits . maxRowsPerRead ) ) , maxRows , dataset . limits . maxRowsPerRead ) ;
const offset = Math . max ( 0 , Number ( options . offset ? ? 0 ) ) ;
const order = options . orderBy ? ident ( options . orderBy , '排序字段' ) : columns . includes ( 'id' ) ? 'id' : null ;
if ( order && ! columns . includes ( order ) ) throw Object . assign ( new Error ( '排序字段未授权' ) , { code : 'columns_not_allowed' } ) ;
return tenant ( async ( client ) => {
const filters = [ ] ;
if ( ( await listTableColumns ( dataset . table ) ) . some ( ( column ) => column . name === 'deleted_at' ) && ! options . includeDeleted ) filters . push ( 'deleted_at IS NULL' ) ;
if ( options . rowScope ? . whereClause ) filters . push ( ` ( ${ options . rowScope . whereClause } ) ` ) ;
const where = filters . length ? ` WHERE ${ filters . join ( ' AND ' ) } ` : '' ;
const result = await client . query (
` SELECT ${ columns . map ( quotePgIdentifier ) . join ( ',' ) } FROM ${ quotePgIdentifier ( dataset . table ) } ${ where } ${ order ? ` ORDER BY ${ quotePgIdentifier ( order ) } ${ String ( options . orderDir ) . toLowerCase ( ) === 'asc' ? 'ASC' : 'DESC' } ` : '' } LIMIT $ 1 OFFSET $ 2 ` ,
[ limit , offset ] ,
) ;
return { dataset , rows : result . rows , limit , offset } ;
} ) ;
}
async function readDatasetRows ( name , options ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return readRowsForDataset ( dataset , options ) ;
}
async function getDatasetStats ( name ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
assertAction ( dataset , 'read' ) ;
return tenant ( async ( client ) => ( { dataset : { name : dataset . name , table : dataset . table } , total : Number ( ( await client . query ( ` SELECT count(*)::bigint AS count FROM ${ quotePgIdentifier ( dataset . table ) } ` ) ) . rows [ 0 ] . count ) } ) ) ;
}
async function insertWithDataset ( dataset , payload , meta = { } ) {
assertAction ( dataset , 'insert' ) ;
const allowed = new Set ( dataset . columns . insert ? ? [ ] ) ;
const keys = Object . keys ( payload ? ? { } ) ;
if ( keys . some ( ( key ) => ! allowed . has ( key ) ) ) throw Object . assign ( new Error ( '提交包含未授权字段' ) , { code : 'columns_not_allowed' } ) ;
return tenant ( async ( client ) => {
const columns = await listTableColumns ( dataset . table ) ;
const values = { ... payload } ;
if ( meta . rowScope ? . ownerColumn && columns . some ( ( c ) => c . name === meta . rowScope . ownerColumn ) ) values [ meta . rowScope . ownerColumn ] ? ? = meta . rowScope . visitorUserId ;
const insertKeys = Object . keys ( values ) . map ( ( key ) => ident ( key , '字段' ) ) ;
const result = await client . query (
` INSERT INTO ${ quotePgIdentifier ( dataset . table ) } ( ${ insertKeys . map ( quotePgIdentifier ) . join ( ',' ) } ) VALUES ( ${ insertKeys . map ( ( _ , i ) => ` $ ${ i + 1 } ` ) . join ( ',' ) } ) RETURNING * ` ,
insertKeys . map ( ( key ) => values [ key ] ) ,
) ;
return { dataset , row : result . rows [ 0 ] } ;
} , { write : true } ) ;
}
async function insertDatasetRow ( name , payload , meta = { } ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return insertWithDataset ( dataset , payload , meta ) ;
}
async function insertRowForDataset ( datasetInput , payload , meta = { } ) {
const dataset = await Promise . resolve ( datasetInput ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return insertWithDataset ( dataset , payload , meta ) ;
}
async function updateRowForDataset ( datasetInput , rowId , payload , meta = { } ) {
const dataset = await Promise . resolve ( datasetInput ) ;
assertAction ( dataset , 'update' ) ;
const id = Number ( rowId ) ;
if ( ! Number . isFinite ( id ) || id <= 0 ) throw Object . assign ( new Error ( '行 id 无效' ) , { code : 'invalid_row_id' } ) ;
const allowed = new Set ( dataset . columns . update ? ? [ ] ) ;
const keys = Object . keys ( payload ? ? { } ) ;
if ( ! keys . length || keys . some ( ( key ) => ! allowed . has ( key ) ) ) throw Object . assign ( new Error ( '更新包含未授权字段' ) , { code : 'columns_not_allowed' } ) ;
return tenant ( async ( client ) => {
const result = await client . query (
` UPDATE ${ quotePgIdentifier ( dataset . table ) } SET ${ keys . map ( ( key , i ) => ` ${ quotePgIdentifier ( ident ( key , '字段' ) ) } = $ ${ i + 1 } ` ) . join ( ',' ) } WHERE id= $ ${ keys . length + 1 } ${ meta . rowScope ? . whereClause ? ` AND ${ meta . rowScope . whereClause } ` : '' } RETURNING * ` ,
[ ... keys . map ( ( key ) => payload [ key ] ) , id ] ,
) ;
if ( ! result . rows [ 0 ] ) throw Object . assign ( new Error ( '无权更新该行或行不存在' ) , { code : 'row_not_allowed' } ) ;
return { dataset , row : result . rows [ 0 ] } ;
} , { write : true } ) ;
}
async function updateDatasetRow ( name , rowId , payload , meta ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return updateRowForDataset ( dataset , rowId , payload , meta ) ;
}
async function softDeleteRowForDataset ( datasetInput , rowId , meta = { } ) {
const dataset = await Promise . resolve ( datasetInput ) ;
assertAction ( dataset , 'soft_delete' ) ;
const columns = await listTableColumns ( dataset . table ) ;
if ( ! columns . some ( ( item ) => item . name === 'deleted_at' ) ) throw Object . assign ( new Error ( '目标表不支持软删除' ) , { code : 'soft_delete_unsupported' } ) ;
const id = Number ( rowId ) ;
return tenant ( async ( client ) => {
const hasDeletedBy = columns . some ( ( item ) => item . name === 'deleted_by' ) ;
const result = await client . query (
` UPDATE ${ quotePgIdentifier ( dataset . table ) } SET deleted_at=CURRENT_TIMESTAMP ${ hasDeletedBy ? ',deleted_by=$2' : '' } WHERE id= $ 1 AND deleted_at IS NULL ${ meta . rowScope ? . whereClause ? ` AND ${ meta . rowScope . whereClause } ` : '' } RETURNING id,deleted_at ` ,
hasDeletedBy ? [ id , meta . deletedBy ? ? meta . updatedByLabel ? ? 'public' ] : [ id ] ,
) ;
if ( ! result . rows [ 0 ] ) throw Object . assign ( new Error ( '无权删除该行或行不存在' ) , { code : 'row_not_allowed' } ) ;
return { dataset , id , deleted : true } ;
} , { write : true } ) ;
}
async function softDeleteDatasetRow ( name , rowId , meta ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return softDeleteRowForDataset ( dataset , rowId , meta ) ;
}
async function restoreSoftDeletedRow ( name , rowId ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
const columns = await listTableColumns ( dataset . table ) ;
return tenant ( async ( client ) => {
const result = await client . query (
` UPDATE ${ quotePgIdentifier ( dataset . table ) } SET deleted_at=NULL ${ columns . some ( ( item ) => item . name === 'deleted_by' ) ? ',deleted_by=NULL' : '' } WHERE id= $ 1 AND deleted_at IS NOT NULL RETURNING * ` ,
[ Number ( rowId ) ] ,
) ;
if ( ! result . rows [ 0 ] ) throw Object . assign ( new Error ( '行不存在或未删除' ) , { code : 'row_not_allowed' } ) ;
return { dataset , row : result . rows [ 0 ] , restored : true } ;
} , { write : true } ) ;
}
async function exportDatasetRows ( name , { format = 'json' , includeDeleted = false , limit = 1000 } = { } ) {
const result = await readDatasetRows ( name , { includeDeleted , limit : Math . min ( Number ( limit ) , 5000 ) } ) ;
if ( String ( format ) . toLowerCase ( ) !== 'csv' ) return { dataset : name , format : 'json' , rows : result . rows , rowCount : result . rows . length } ;
const columns = result . dataset . columns . read ? ? [ ] ;
const escape = ( value ) => {
const text = value == null ? '' : String ( value ) ;
return /[",\n\r]/ . test ( text ) ? ` " ${ text . replaceAll ( '"' , '""' ) } " ` : text ;
} ;
return { dataset : name , format : 'csv' , content : ` ${ columns . join ( ',' ) } \n ${ result . rows . map ( ( row ) => columns . map ( ( column ) => escape ( row [ column ] ) ) . join ( ',' ) ) . join ( '\n' ) } \n ` , rowCount : result . rows . length } ;
}
async function getDatasetSchema ( name ) {
const dataset = await getDataset ( name ) ;
if ( ! dataset ) throw Object . assign ( new Error ( 'dataset 不存在' ) , { code : 'dataset_not_found' } ) ;
return { dataset , tableColumns : await listTableColumns ( dataset . table ) } ;
}
async function getInfo ( ) {
return { name : '用户私有数据空间' , backend : 'postgres' , database : poolConfig ( options ) . database , schema : names . schemaName , userId : names . userId } ;
}
async function ensureReady ( ) {
await ensureProvisioned ( { force : true } ) ;
return getInfo ( ) ;
}
return {
backend : 'postgres' , workspaceRoot : options . workspaceRoot , privateDataDb : null ,
ensureReady , getInfo , getSchema , querySql , executeSql , listTableColumns , getDataset , listDatasets , upsertDataset ,
readRowsForDataset , readDatasetRows , getDatasetStats , getStatsForDataset : async ( dataset ) => getDatasetStats ( ( await Promise . resolve ( dataset ) ) . name ) ,
getDatasetSchema , getSchemaForDataset : async ( dataset ) => ( { dataset : await Promise . resolve ( dataset ) , tableColumns : await listTableColumns ( ( await Promise . resolve ( dataset ) ) . table ) } ) ,
insertDatasetRow , insertRowForDataset , updateDatasetRow , updateRowForDataset ,
softDeleteDatasetRow , softDeleteRowForDataset , restoreSoftDeletedRow , exportDatasetRows ,
} ;
}