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 <noreply@anthropic.com>
This commit is contained in:
@@ -437,3 +437,148 @@ export function findMatch(
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── DB Operations ────────────────────────────────────────────────────────────
|
||||
|
||||
async function upsertChurch(
|
||||
entry: ParsedEntry,
|
||||
matched: ExistingChurch | null,
|
||||
dryRun: boolean,
|
||||
stats: ImportStats
|
||||
): Promise<void> {
|
||||
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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user