From 92265cf27ff72bc029cdf790909bf65ed1d55d90 Mon Sep 17 00:00:00 2001 From: albertfj114 Date: Fri, 3 Apr 2026 16:27:02 -0400 Subject: [PATCH] feat: add DB operations and CLI wiring for HK parish import upsertChurch() handles matched churches (replace schedules atomically via $transaction, update contact fields if null) and new churches (create with source='diocese-hk', lat/lng=0 for later geocoding). main() wires up CLI args, file reading, matching loop, and summary. Guards main() call with ESM import.meta.url check to prevent execution on import during tests. Co-Authored-By: Claude Sonnet 4.6 --- scripts/import-hk-parishes.ts | 145 ++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/scripts/import-hk-parishes.ts b/scripts/import-hk-parishes.ts index 4c7d2de..8ab22a3 100644 --- a/scripts/import-hk-parishes.ts +++ b/scripts/import-hk-parishes.ts @@ -437,3 +437,148 @@ export function findMatch( return null; } + +// ─── DB Operations ──────────────────────────────────────────────────────────── + +async function upsertChurch( + entry: ParsedEntry, + matched: ExistingChurch | null, + dryRun: boolean, + stats: ImportStats +): Promise { + const tag = matched ? `[MATCH] ${matched.name} ← ${entry.locationName}` : `[NEW] ${entry.locationName}`; + const schedCount = entry.schedules.length; + + if (dryRun) { + console.log(tag); + if (!matched && entry.address) console.log(` Address: ${entry.address}`); + console.log(` ${schedCount} schedules`); + if (matched) stats.matched++; else stats.created++; + stats.schedulesWritten += schedCount; + return; + } + + if (matched) { + const update: Record = {}; + if (!matched.phone && entry.phone) update.phone = entry.phone; + if (!matched.email && entry.email) update.email = entry.email; + + await prisma.$transaction(async tx => { + if (Object.keys(update).length > 0) { + await tx.church.update({ where: { id: matched.id }, data: update }); + } + await tx.massSchedule.deleteMany({ where: { churchId: matched.id } }); + if (entry.schedules.length > 0) { + await tx.massSchedule.createMany({ + data: entry.schedules.map(s => ({ + churchId: matched.id, + dayOfWeek: s.dayOfWeek, + time: s.time, + language: s.language, + notes: s.notes ?? null, + })), + }); + } + }); + + stats.matched++; + } else { + const newChurch = await prisma.church.create({ + data: { + name: entry.locationName, + country: 'HK', + source: 'diocese-hk', + address: entry.address ?? undefined, + phone: entry.phone ?? undefined, + email: entry.email ?? undefined, + latitude: 0, + longitude: 0, + hasWebsite: false, + }, + }); + + if (entry.schedules.length > 0) { + await prisma.massSchedule.createMany({ + data: entry.schedules.map(s => ({ + churchId: newChurch.id, + dayOfWeek: s.dayOfWeek, + time: s.time, + language: s.language, + notes: s.notes ?? null, + })), + }); + } + + stats.created++; + } + + stats.schedulesWritten += schedCount; + console.log(tag); +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +async function main() { + const args = process.argv.slice(2); + const dryRun = args.includes('--dry-run'); + const fileArgIdx = args.indexOf('--file'); + const filePath = fileArgIdx >= 0 ? args[fileArgIdx + 1] : path.resolve(process.cwd(), 'scripts/hk-parishes.txt'); + + console.log(`\n${'='.repeat(60)}`); + console.log(`HK Diocese Parish Import`); + console.log(`File: ${filePath}`); + console.log(`Dry run: ${dryRun ? 'Yes' : 'No'}`); + console.log(`${'='.repeat(60)}\n`); + + const raw = fs.readFileSync(filePath, 'utf-8'); + const entryStrings = splitEntries(raw); + console.log(`Found ${entryStrings.length} entries in file\n`); + + const existing = await prisma.church.findMany({ + where: { country: 'HK' }, + select: { id: true, name: true, address: true, phone: true, email: true }, + }); + console.log(`Loaded ${existing.length} existing HK churches\n`); + + const stats: ImportStats = { matched: 0, created: 0, schedulesWritten: 0, skipped: 0 }; + + for (const entryStr of entryStrings) { + let entry: ParsedEntry; + try { + entry = parseEntry(entryStr); + } catch (err) { + console.warn(`[SKIP] Failed to parse entry: ${(err as Error).message}`); + stats.skipped++; + continue; + } + + if (!entry.locationName || entry.locationName === 'Unknown') { + stats.skipped++; + continue; + } + + const matched = findMatch(entry.locationName, entry.address, existing); + await upsertChurch(entry, matched, dryRun, stats); + } + + console.log(`\n${'='.repeat(60)}`); + console.log(`Import Summary`); + console.log(`${'='.repeat(60)}`); + console.log(`Matched existing: ${stats.matched}`); + console.log(`New churches: ${stats.created}`); + console.log(`Skipped: ${stats.skipped}`); + console.log(`Schedules written: ${stats.schedulesWritten}`); + console.log(`${'='.repeat(60)}\n`); + + await prisma.$disconnect(); + await pool.end(); +} + +// Only run when executed directly (not imported by tests) +import { fileURLToPath } from 'url'; +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main().catch(err => { + console.error('Fatal error:', err); + process.exit(1); + }); +}