145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* One-shot migration: legacy NocoDB Interests → new client/interest split.
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
*
|
||
|
|
* pnpm tsx scripts/migrate-from-nocodb.ts --dry-run
|
||
|
|
* Pulls the live NocoDB base, runs the transform + dedup pipeline,
|
||
|
|
* writes a report to .migration/<timestamp>/. NO database writes.
|
||
|
|
*
|
||
|
|
* pnpm tsx scripts/migrate-from-nocodb.ts --dry-run --port-slug harbor-royale
|
||
|
|
* Same, but tags the planned writes with the named port (matters for
|
||
|
|
* the apply phase — every client/interest belongs to one port).
|
||
|
|
*
|
||
|
|
* pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration/<dir>/
|
||
|
|
* [Not yet implemented — apply phase comes in a follow-up PR.]
|
||
|
|
*
|
||
|
|
* Design reference: docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import 'dotenv/config';
|
||
|
|
|
||
|
|
import path from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
import { fetchSnapshot, loadNocoDbConfig } from '@/lib/dedup/nocodb-source';
|
||
|
|
import { transformSnapshot } from '@/lib/dedup/migration-transform';
|
||
|
|
import { resolveReportPaths, writeReport } from '@/lib/dedup/migration-report';
|
||
|
|
|
||
|
|
interface CliArgs {
|
||
|
|
dryRun: boolean;
|
||
|
|
apply: boolean;
|
||
|
|
portSlug: string | null;
|
||
|
|
reportDir: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseArgs(argv: string[]): CliArgs {
|
||
|
|
const args: CliArgs = {
|
||
|
|
dryRun: false,
|
||
|
|
apply: false,
|
||
|
|
portSlug: null,
|
||
|
|
reportDir: null,
|
||
|
|
};
|
||
|
|
for (let i = 0; i < argv.length; i += 1) {
|
||
|
|
const a = argv[i]!;
|
||
|
|
if (a === '--dry-run') args.dryRun = true;
|
||
|
|
else if (a === '--apply') args.apply = true;
|
||
|
|
else if (a === '--port-slug') args.portSlug = argv[++i] ?? null;
|
||
|
|
else if (a === '--report') args.reportDir = argv[++i] ?? null;
|
||
|
|
else if (a === '-h' || a === '--help') {
|
||
|
|
printHelp();
|
||
|
|
process.exit(0);
|
||
|
|
} else {
|
||
|
|
console.error(`Unknown argument: ${a}`);
|
||
|
|
printHelp();
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return args;
|
||
|
|
}
|
||
|
|
|
||
|
|
function printHelp(): void {
|
||
|
|
console.log(`Usage:
|
||
|
|
pnpm tsx scripts/migrate-from-nocodb.ts --dry-run [--port-slug <slug>]
|
||
|
|
Pulls NocoDB → transforms → writes report to .migration/<timestamp>/.
|
||
|
|
No database writes.
|
||
|
|
|
||
|
|
pnpm tsx scripts/migrate-from-nocodb.ts --apply --report .migration/<dir>/
|
||
|
|
Apply phase. (Not yet implemented.)
|
||
|
|
|
||
|
|
Flags:
|
||
|
|
--dry-run Read NocoDB, write report only.
|
||
|
|
--apply Actually write to the new DB. (Not yet supported.)
|
||
|
|
--port-slug <slug> Port slug to attach to all imported entities.
|
||
|
|
Defaults to the first available port if omitted.
|
||
|
|
--report <dir> Path to a previously-generated report dir
|
||
|
|
(only used by --apply).
|
||
|
|
-h, --help Show this help.
|
||
|
|
`);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main(): Promise<void> {
|
||
|
|
const args = parseArgs(process.argv.slice(2));
|
||
|
|
|
||
|
|
if (!args.dryRun && !args.apply) {
|
||
|
|
console.error('Must specify --dry-run or --apply');
|
||
|
|
printHelp();
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (args.apply) {
|
||
|
|
console.error('--apply is not yet implemented in this version. P3 ships dry-run first.');
|
||
|
|
console.error('See docs/superpowers/specs/2026-05-03-dedup-and-migration-design.md §9.2.');
|
||
|
|
process.exit(2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Dry-run path ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
console.log('[migrate] Loading NocoDB config…');
|
||
|
|
const config = loadNocoDbConfig();
|
||
|
|
console.log(`[migrate] Source: ${config.url}`);
|
||
|
|
|
||
|
|
console.log('[migrate] Fetching snapshot from NocoDB…');
|
||
|
|
const start = Date.now();
|
||
|
|
const snapshot = await fetchSnapshot(config);
|
||
|
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||
|
|
console.log(
|
||
|
|
`[migrate] Snapshot fetched in ${elapsed}s — ${snapshot.interests.length} interests, ${snapshot.residentialInterests.length} residential, ${snapshot.berths.length} berths.`,
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log('[migrate] Running transform + dedup pipeline…');
|
||
|
|
const plan = transformSnapshot(snapshot);
|
||
|
|
|
||
|
|
// Resolve output paths relative to the worktree root (the script itself
|
||
|
|
// lives in scripts/; we want the .migration dir at the repo root).
|
||
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
const repoRoot = path.resolve(scriptDir, '..');
|
||
|
|
const generatedAt = new Date().toISOString();
|
||
|
|
const paths = resolveReportPaths(repoRoot);
|
||
|
|
|
||
|
|
console.log(`[migrate] Writing report to ${paths.rootDir}…`);
|
||
|
|
await writeReport(paths, plan, generatedAt);
|
||
|
|
|
||
|
|
// ── Console summary ──────────────────────────────────────────────────────
|
||
|
|
const s = plan.stats;
|
||
|
|
console.log('');
|
||
|
|
console.log('=== Migration Plan Summary ===');
|
||
|
|
console.log(
|
||
|
|
` Input: ${s.inputInterestRows} interests, ${s.inputResidentialRows} residential interests`,
|
||
|
|
);
|
||
|
|
console.log(` Output: ${s.outputClients} clients, ${s.outputInterests} interests`);
|
||
|
|
console.log(` ${s.outputContacts} contacts, ${s.outputAddresses} addresses`);
|
||
|
|
console.log(
|
||
|
|
` Dedup: ${s.autoLinkedClusters} auto-linked clusters, ${s.needsReviewPairs} pairs flagged for review`,
|
||
|
|
);
|
||
|
|
console.log(` Quality: ${s.flaggedRows} rows flagged (see report.csv)`);
|
||
|
|
console.log('');
|
||
|
|
console.log(` Full report: ${paths.summaryPath}`);
|
||
|
|
console.log('');
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch((err) => {
|
||
|
|
console.error('[migrate] Fatal error:', err);
|
||
|
|
process.exit(1);
|
||
|
|
});
|