#!/usr/bin/env node import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import { callSpacetimeProcedure, callSpacetimeProcedureViaCli, createSpacetimeWebIdentity, ensureParentDir, ensureProcedureOk, parseArgs, } from './spacetime-migration-common.mjs'; try { const options = parseArgs(process.argv.slice(2)); if (!options.out) { throw new Error('必须传入 --out。'); } const input = { include_tables: options.includeTables, }; const webOptions = await prepareWebExportOptions(options); let result; try { result = await callSpacetimeProcedure(webOptions, 'export_database_migration_to_file', input); } finally { await revokeTemporaryWebIdentity(webOptions); } ensureProcedureOk(result); if (typeof result.migration_json !== 'string' || result.migration_json.trim() === '') { throw new Error('导出 procedure 没有返回 migration_json。'); } const outPath = path.resolve(options.out); await ensureParentDir(outPath); await writeFile(outPath, result.migration_json, 'utf8'); console.log(`[spacetime:migration:export] 已写入 ${outPath}`); printTableStats(result.table_stats); printMigrationWarnings(result.warnings); } catch (error) { console.error( `[spacetime:migration:export] ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } async function prepareWebExportOptions(options) { if (options.token) { return { ...options, useHttp: true }; } const identity = await createSpacetimeWebIdentity(options); console.log( `[spacetime:migration:export] 已通过 Web API 创建临时 identity: ${identity.identity}`, ); try { const authorizeResult = await callSpacetimeProcedureViaCli( options, 'authorize_database_migration_operator', { bootstrap_secret: options.bootstrapSecret || '', operator_identity_hex: identity.identity, note: options.note || 'temporary web api migration export', }, ); ensureProcedureOk(authorizeResult); } catch (error) { throw new Error( `授权临时 Web API identity 失败。当前 spacetime CLI identity 必须已经是迁移操作员;如果旧库迁移操作员表不为空,bootstrap secret 不会越权授权新的操作员。可先用已有迁移操作员授权当前部署机 identity,或为导出脚本提供已有迁移操作员的 --token。原始错误: ${ error instanceof Error ? error.message : String(error) }`, ); } console.log(`[spacetime:migration:export] 已授权临时 Web API identity`); return { ...options, token: identity.token, temporaryWebIdentity: identity.identity, useHttp: true, }; } async function revokeTemporaryWebIdentity(options) { if (!options.temporaryWebIdentity) { return; } try { const revokeResult = await callSpacetimeProcedure( options, 'revoke_database_migration_operator', { operator_identity_hex: options.temporaryWebIdentity }, ); ensureProcedureOk(revokeResult); console.log(`[spacetime:migration:export] 已撤销临时 Web API identity`); } catch (error) { console.warn( `[spacetime:migration:export] 撤销临时 Web API identity 失败: ${ error instanceof Error ? error.message : String(error) }`, ); } } function printTableStats(tableStats) { if (!Array.isArray(tableStats) || tableStats.length === 0) { return; } const rows = tableStats.map((stat) => ({ table: stat.table_name, exported: stat.exported_row_count, })); console.table(rows); } function printMigrationWarnings(warnings) { if (!Array.isArray(warnings) || warnings.length === 0) { return; } console.warn('[spacetime:migration:export] 迁移告警:'); console.table( warnings.map((warning) => ({ table: warning.table_name, kind: warning.warning_kind, message: warning.message, })), ); }