/** * One-off import of historical "Website – Contact Form Submissions" from NocoDB * into the CRM `website_submissions` table, so they show up in the Inquiries * workbench alongside post-cutover submissions. * * The cutover migration imported interests / residential / berths / expenses but * NOT the contact-form table — those general contact-page inquiries (the * "broker"/"investor"/"owner" enquiries) were left behind in NocoDB. * * Idempotent: each row maps to a deterministic `submission_id` * (`nocodb-cf-`) guarded by the unique index, plus a `migration_source_links` * ledger row (`source_system='nocodb_website_submissions'`). Re-running is a * no-op for already-imported rows. * * Usage: * pnpm tsx scripts/import-website-inquiries-from-nocodb.ts # dry-run * pnpm tsx scripts/import-website-inquiries-from-nocodb.ts --apply # write * pnpm tsx scripts/import-website-inquiries-from-nocodb.ts --apply --port-slug port-nimara * * Requires NOCODB_URL + NOCODB_TOKEN in env (same as the migration). Writes to * whatever DATABASE_URL points at — point it at prod ONLY with explicit approval. */ import 'dotenv/config'; import { eq } from 'drizzle-orm'; import { db, closeDb } from '@/lib/db'; import { ports } from '@/lib/db/schema/ports'; import { websiteSubmissions } from '@/lib/db/schema/website-submissions'; import { migrationSourceLinks } from '@/lib/db/schema/migration'; import { loadNocoDbConfig, fetchAllRows, NOCO_TABLES, type NocoDbRow, } from '@/lib/dedup/nocodb-source'; const SOURCE_SYSTEM = 'nocodb_website_submissions'; const APPLIED_ID = 'import-website-inquiries'; function arg(name: string): string | undefined { const hit = process.argv.find((a) => a.startsWith(`--${name}=`)); if (hit) return hit.split('=')[1]; const idx = process.argv.indexOf(`--${name}`); if (idx !== -1 && process.argv[idx + 1] && !process.argv[idx + 1]!.startsWith('--')) { return process.argv[idx + 1]; } return undefined; } function str(row: NocoDbRow, ...keys: string[]): string { for (const k of keys) { const v = row[k]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; } function parseDate(row: NocoDbRow): Date { const raw = str(row, 'CreatedAt', 'created_at', 'Created At', 'createdAt'); if (raw) { const d = new Date(raw); if (!Number.isNaN(d.getTime())) return d; } return new Date(); } async function main() { const apply = process.argv.includes('--apply'); const portSlug = arg('port-slug') ?? 'port-nimara'; const [port] = await db .select({ id: ports.id }) .from(ports) .where(eq(ports.slug, portSlug)) .limit(1); if (!port) throw new Error(`Unknown port slug: ${portSlug}`); const config = loadNocoDbConfig(); console.log(`[import] Fetching contact-form submissions from NocoDB…`); const rows = await fetchAllRows(NOCO_TABLES.websiteContactFormSubmissions, config); console.log(`[import] Fetched ${rows.length} rows from NocoDB.`); let inserted = 0; let skipped = 0; const samples: Array> = []; for (const row of rows) { const legacyId = String(row.Id); const submissionId = `nocodb-cf-${legacyId}`; const fullName = str(row, 'Full Name', 'Name', 'full_name'); const email = str(row, 'Email Address', 'Email', 'email'); const interest = str(row, 'Type of Interest', 'interest'); const comments = str(row, 'Comments', 'comments'); const receivedAt = parseDate(row); const payload = { name: fullName, email, interest, comments, imported_from: 'nocodb_contact_form', legacy_nocodb_id: legacyId, }; if (samples.length < 3) { samples.push({ submissionId, fullName, email, interest, receivedAt: receivedAt.toISOString(), }); } if (!apply) { // Dry-run: count how many are not yet present. const [existing] = await db .select({ id: websiteSubmissions.id }) .from(websiteSubmissions) .where(eq(websiteSubmissions.submissionId, submissionId)) .limit(1); if (existing) skipped += 1; else inserted += 1; continue; } const result = await db .insert(websiteSubmissions) .values({ portId: port.id, submissionId, kind: 'contact_form', payload, contactName: fullName || null, contactEmail: email || null, legacyNocodbId: legacyId, receivedAt, triageState: 'open', }) .onConflictDoNothing({ target: websiteSubmissions.submissionId }) .returning({ id: websiteSubmissions.id }); if (result[0]) { inserted += 1; await db .insert(migrationSourceLinks) .values({ sourceSystem: SOURCE_SYSTEM, sourceId: legacyId, targetEntityType: 'website_submission', targetEntityId: result[0].id, appliedId: APPLIED_ID, }) .onConflictDoNothing(); } else { skipped += 1; } } console.log('\n[import] Sample rows:'); for (const s of samples) console.log(' ', JSON.stringify(s)); console.log( `\n[import] ${apply ? 'APPLIED' : 'DRY-RUN'} — port=${portSlug}: ${inserted} ${ apply ? 'inserted' : 'would insert' }, ${skipped} skipped (already present).`, ); if (!apply) console.log('[import] Re-run with --apply to write these rows.'); await closeDb(); } main().catch((err) => { console.error('[import] FAILED:', err); process.exitCode = 1; });