88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
import { readFile } from 'node:fs/promises';
|
|
import postgres from 'postgres';
|
|
|
|
// Seeds the Worldwide Todo List (schema worldtodo) from data/worldtodo-seed.json, built by
|
|
// scripts/build-worldtodo-seed.py (68 sourced tasks as of 2026-10-26). Idempotent: categories
|
|
// are upserted, tasks are inserted with ON CONFLICT (slug) DO NOTHING, so owner edits and
|
|
// approved deletes (soft, the row stays) are never overwritten. Runs on every deploy
|
|
// (scripts/deploy-remote.sh) as the schema owner; self-contained so it works in the app image.
|
|
// bun scripts/seed-worldtodo.ts
|
|
|
|
if (!process.env.DB_URL) throw new Error('DB_URL is required');
|
|
|
|
type SeedTask = {
|
|
slug: string;
|
|
title: string;
|
|
description: string;
|
|
scale: string;
|
|
category: string;
|
|
location_name: string;
|
|
country_code: string | null;
|
|
lat: number | null;
|
|
lon: number | null;
|
|
status: string;
|
|
source_urls: string[];
|
|
source_date: string | null;
|
|
as_of: string;
|
|
confidence: string;
|
|
verification_note?: string | null;
|
|
};
|
|
|
|
// Lucide icon key (#lib/worldtodo/icons) and an accent colour for the icon tile.
|
|
const CATEGORIES: Record<string, { label: string; icon: string; colour: string }> = {
|
|
climate: { label: 'Climate', icon: 'thermometer', colour: '#ef4444' },
|
|
environment: { label: 'Environment', icon: 'leaf', colour: '#22c55e' },
|
|
energy: { label: 'Energy', icon: 'zap', colour: '#eab308' },
|
|
food: { label: 'Food', icon: 'wheat', colour: '#d97706' },
|
|
governance: { label: 'Governance', icon: 'landmark', colour: '#6366f1' },
|
|
health: { label: 'Health', icon: 'heart-pulse', colour: '#ec4899' },
|
|
housing: { label: 'Housing', icon: 'house', colour: '#f97316' },
|
|
humanitarian: { label: 'Humanitarian', icon: 'hand-heart', colour: '#e11d48' },
|
|
infrastructure: { label: 'Infrastructure', icon: 'construction', colour: '#0ea5e9' },
|
|
mobility: { label: 'Mobility', icon: 'bike', colour: '#14b8a6' },
|
|
safety: { label: 'Safety', icon: 'shield-alert', colour: '#f59e0b' },
|
|
waste: { label: 'Waste', icon: 'recycle', colour: '#84cc16' },
|
|
water: { label: 'Water', icon: 'droplet', colour: '#3b82f6' }
|
|
};
|
|
|
|
const seed = JSON.parse(
|
|
await readFile(new URL('../data/worldtodo-seed.json', import.meta.url), 'utf8')
|
|
) as { categories: string[]; tasks: SeedTask[] };
|
|
|
|
const client = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
|
|
try {
|
|
await client.begin(async (tx) => {
|
|
for (const slug of seed.categories) {
|
|
const c = CATEGORIES[slug] ?? { label: slug, icon: 'list-todo', colour: '#a3a3a3' };
|
|
await tx`insert into worldtodo.categories (slug, label, icon, colour)
|
|
values (${slug}, ${c.label}, ${c.icon}, ${c.colour})
|
|
on conflict (slug) do update set label = excluded.label, icon = excluded.icon,
|
|
colour = excluded.colour`;
|
|
}
|
|
let inserted = 0;
|
|
for (const t of seed.tasks) {
|
|
const rows = await tx`
|
|
insert into worldtodo.tasks (slug, title, description, scale, category, location_name,
|
|
country_code, lat, lon, status, source_urls, source_date, as_of, confidence,
|
|
verification_note)
|
|
values (${t.slug}, ${t.title}, ${t.description}, ${t.scale}::worldtodo.scale,
|
|
${t.category}, ${t.location_name}, ${t.country_code ?? null}, ${t.lat ?? null},
|
|
${t.lon ?? null}, ${t.status}::worldtodo.status, ${t.source_urls},
|
|
${t.source_date ?? null}, ${t.as_of}, ${t.confidence ?? 'high'},
|
|
${t.verification_note ?? null})
|
|
on conflict (slug) do nothing
|
|
returning id`;
|
|
inserted += rows.length;
|
|
}
|
|
const [{ n }] = await tx`select count(*)::int as n from worldtodo.tasks
|
|
where slug = any(${seed.tasks.map((t) => t.slug)})`;
|
|
if (n !== seed.tasks.length)
|
|
throw new Error(`Expected ${seed.tasks.length} seed tasks, found ${n}`);
|
|
console.log(
|
|
`Worldwide todo: ${seed.categories.length} categories, ${inserted} new of ${seed.tasks.length} seed tasks (all present)`
|
|
);
|
|
});
|
|
} finally {
|
|
await client.end();
|
|
}
|