/** * Import health data (JSON or CSV) into the private `health` schema. * * bun scripts/health-import.ts data.json # {"vaccinations":[...],"sleep":[...]} * bun scripts/health-import.ts --table vaccinations rivm.csv # CSV (header row) or a JSON array * bun scripts/health-import.ts --table sleep - < sleep.csv # stdin * add --dry-run to validate without writing * * Column names are the snake_case DB columns (see src/lib/health/tables.ts). Rows with an * `id` update that row; nutrition_daily and sleep upsert on `date`; body_measurements dedupe * on measured_at or (date, weight_kg) and fill smart-scale columns from the notes, so * re-importing is a no-op; everything else is inserted (importing twice duplicates those). All-or-nothing: if any * row is invalid, nothing is written. Uses DB_URL (the app role on prod, personal_dev on dev). */ import { readFile } from 'node:fs/promises'; import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import { parseCsv, toPayload, validatePayload, writeRows } from '../src/lib/server/health-import'; let dryRun = false; let table: string | undefined; const files: string[] = []; for (let i = 2; i < process.argv.length; i++) { const arg = process.argv[i]; if (arg === '--dry-run') dryRun = true; else if (arg === '--table') table = process.argv[++i]; else if (arg.startsWith('--table=')) table = arg.slice(8); else files.push(arg); } if (!files.length) { console.error( 'usage: bun scripts/health-import.ts [--table ] [--dry-run] ...' ); process.exit(2); } const read = (file: string) => (file === '-' ? Bun.stdin.text() : readFile(file, 'utf8')); const merged: Record = {}; try { for (const file of files) { const text = await read(file); const csv = file.toLowerCase().endsWith('.csv') || (file === '-' && !/^\s*[[{]/.test(text)); const payload = toPayload(csv ? parseCsv(text) : JSON.parse(text), table); for (const [t, rows] of Object.entries(payload)) (merged[t] ??= []).push(...(rows ?? [])); } } catch (e) { console.error(e instanceof Error ? e.message : e); process.exit(1); } const { clean, errors } = validatePayload(toPayload(merged)); if (errors.length) { for (const e of errors.slice(0, 50)) console.error(`${e.table} row ${e.row}: ${e.error}`); console.error(`${errors.length} invalid row(s); nothing imported.`); process.exit(1); } const summary = Object.entries(clean) .map(([t, r]) => `${t}: ${r.length}`) .join(', '); if (dryRun) { console.log(`Valid (dry run, nothing written): ${summary}`); process.exit(0); } if (!process.env.DB_URL) throw new Error('DB_URL is required'); const client = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} }); try { const counts = await writeRows(drizzle({ client }), clean); for (const [t, c] of Object.entries(counts)) console.log(`${t}: ${c.inserted} inserted, ${c.updated} updated, ${c.unchanged} unchanged`); } finally { await client.end(); }