All checks were successful
Deploy production / deploy (push) Successful in 2m32s
74 lines
2.9 KiB
TypeScript
74 lines
2.9 KiB
TypeScript
/**
|
|
* 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 <name>] [--dry-run] <file.json|file.csv|->...'
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
const read = (file: string) => (file === '-' ? Bun.stdin.text() : readFile(file, 'utf8'));
|
|
const merged: Record<string, unknown[]> = {};
|
|
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();
|
|
}
|