lilith-platform.live/codebase/@features/api/scripts/migrate-from-quinn-admin.ts
2026-05-12 15:57:20 -07:00

112 lines
3.8 KiB
TypeScript

/**
* One-shot data migration: quinn_admin (legacy admin DB) → quinn (Quinn API DB).
*
* Uses raw postgres() to avoid the openDb singleton (which caches by global, not URL).
*
* Migrates tables that have data in prod (as of 2026-05-12):
* - about (1 row)
* - link_values (57 rows)
*
* Tables with 0 rows in prod: activity_menus, etiquette_*, policy_*,
* hero_strip_items, positioning_tags. Skipped.
*
* Env vars:
* QUINN_ADMIN_DB_URL — source
* QUINN_DB_URL — destination
* DRY_RUN=1 — read counts only
* PROVIDER_SLUG — default 'quinn'
*/
import postgres from 'postgres';
import { logger } from '@/shared/logger';
const SOURCE_URL = process.env['QUINN_ADMIN_DB_URL'] ?? '';
const DEST_URL = process.env['QUINN_DB_URL'] ?? '';
const DRY_RUN = process.env['DRY_RUN'] === '1';
const PROVIDER_SLUG = process.env['PROVIDER_SLUG'] ?? 'quinn';
interface AdminAboutRow {
readonly bio: string;
readonly personality: string | null;
readonly available_for: string | null;
readonly available_to: string | null;
}
interface AdminLinkValueRow {
readonly event_name: string;
readonly label: string;
readonly score: number;
}
async function main(): Promise<void> {
if (!SOURCE_URL || !DEST_URL) {
logger.error('QUINN_ADMIN_DB_URL and QUINN_DB_URL are required');
process.exit(2);
}
const src = postgres(SOURCE_URL, { max: 5, idle_timeout: 10, connect_timeout: 10 });
const dst = postgres(DEST_URL, { max: 5, idle_timeout: 10, connect_timeout: 10 });
logger.info('Migration starting', { dryRun: DRY_RUN, providerSlug: PROVIDER_SLUG });
try {
// --- about (1 row in prod) ---
const aboutRows = await src<AdminAboutRow[]>`SELECT bio, personality, available_for, available_to FROM about WHERE id = 1`;
logger.info('source about rows', { count: aboutRows.length });
if (aboutRows.length > 0 && !DRY_RUN) {
const a = aboutRows[0]!;
await dst.unsafe(`
INSERT INTO about (provider_slug, bio, personality, available_for, available_to, updated_at)
VALUES ($1, $2, $3, $4, $5, now())
ON CONFLICT (provider_slug) DO UPDATE
SET bio = EXCLUDED.bio,
personality = EXCLUDED.personality,
available_for = EXCLUDED.available_for,
available_to = EXCLUDED.available_to,
updated_at = now()
`, [
PROVIDER_SLUG,
a.bio,
a.personality ?? '[]',
a.available_for ?? '[]',
a.available_to ?? '[]',
]);
logger.info('about migrated');
}
// --- link_values (57 rows in prod) ---
const linkRows = await src<AdminLinkValueRow[]>`SELECT event_name, label, score FROM link_values`;
logger.info('source link_value rows', { count: linkRows.length });
if (linkRows.length > 0 && !DRY_RUN) {
let upserted = 0;
for (const r of linkRows) {
await dst.unsafe(`
INSERT INTO link_values (event_name, label, score, updated_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (event_name, label) DO UPDATE
SET score = EXCLUDED.score, updated_at = now()
`, [r.event_name, r.label, r.score]);
upserted++;
}
logger.info('link_values migrated', { upserted });
}
// --- verify ---
const dstAbout = await dst<Array<{ count: string }>>`SELECT count(*)::text AS count FROM about`;
const dstLink = await dst<Array<{ count: string }>>`SELECT count(*)::text AS count FROM link_values`;
logger.info('Destination counts', {
about: dstAbout[0]?.count,
link_values: dstLink[0]?.count,
});
} finally {
await src.end();
await dst.end();
}
logger.info('Migration complete');
}
main().catch((err: unknown) => {
logger.error('Migration failed', { err: String(err) });
process.exit(1);
});