feat(mindspace): enforce PostgreSQL user data delivery
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import {
|
||||
buildControlSchemaSql,
|
||||
createSqliteSnapshot,
|
||||
inspectSqliteDatabase,
|
||||
migrateSqliteSnapshot,
|
||||
provisionUserSpace,
|
||||
} from '../mindspace-userdata-postgres.mjs';
|
||||
|
||||
function usage() {
|
||||
return `
|
||||
Usage:
|
||||
node scripts/migrate-mindspace-sqlite-to-postgres.mjs --source <sqlite> --user-id <uuid> [--apply] [--replace-shadow]
|
||||
|
||||
Default is read-only dry-run. --apply requires MINDSPACE_USERDATA_PG_URL.
|
||||
The command creates a consistent SQLite snapshot and never modifies the source SQLite.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
export function parseArgs(argv = []) {
|
||||
const options = { apply: false, replaceShadow: false, source: '', userId: '', help: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--apply') options.apply = true;
|
||||
else if (arg === '--replace-shadow') options.replaceShadow = true;
|
||||
else if (arg === '--source') options.source = argv[++index] ?? '';
|
||||
else if (arg === '--user-id') options.userId = argv[++index] ?? '';
|
||||
else if (arg === '--help' || arg === '-h') options.help = true;
|
||||
else throw new Error(`未知参数:${arg}`);
|
||||
}
|
||||
if (!options.help && (!options.source || !options.userId)) throw new Error('--source 和 --user-id 必填');
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function run(argv = process.argv.slice(2), env = process.env) {
|
||||
const options = parseArgs(argv);
|
||||
if (options.help) {
|
||||
console.log(usage());
|
||||
return { mode: 'help' };
|
||||
}
|
||||
const inspected = inspectSqliteDatabase(options.source);
|
||||
const sourceRows = inspected.tables.reduce((total, table) => total + table.rows.length, 0);
|
||||
if (!options.apply) {
|
||||
console.log(JSON.stringify({
|
||||
mode: 'dry-run',
|
||||
userId: options.userId,
|
||||
source: inspected.path,
|
||||
sizeBytes: inspected.sizeBytes,
|
||||
tables: inspected.tables.map((table) => ({ name: table.name, rows: table.rows.length })),
|
||||
sourceRows,
|
||||
note: '未修改 SQLite 或 PostgreSQL;使用 --apply 才会创建影子空间。',
|
||||
}, null, 2));
|
||||
return { mode: 'dry-run', tableCount: inspected.tables.length, sourceRows };
|
||||
}
|
||||
if (!env.MINDSPACE_USERDATA_PG_URL) throw new Error('--apply requires MINDSPACE_USERDATA_PG_URL');
|
||||
const snapshotPath = createSqliteSnapshot(options.source);
|
||||
const snapshot = inspectSqliteDatabase(snapshotPath);
|
||||
const { default: pg } = await import('pg');
|
||||
const client = new pg.Client({ connectionString: env.MINDSPACE_USERDATA_PG_URL });
|
||||
try {
|
||||
await client.connect();
|
||||
await client.query(buildControlSchemaSql());
|
||||
await provisionUserSpace(client, options.userId, {
|
||||
sourceSqlitePath: inspected.path,
|
||||
});
|
||||
const result = await migrateSqliteSnapshot(client, snapshot, options.userId, {
|
||||
replaceShadow: options.replaceShadow,
|
||||
sourcePath: inspected.path,
|
||||
});
|
||||
if (result.sourceRows !== result.targetRows) throw new Error('迁移行数校验失败');
|
||||
console.log(JSON.stringify({ mode: 'shadow', ...result }, null, 2));
|
||||
return { mode: 'shadow', ...result };
|
||||
} finally {
|
||||
await client.end().catch(() => {});
|
||||
fs.rmSync(snapshotPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
run().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user