Files
anatomia/data-pipeline/scripts/seed-db.mjs
T

118 lines
4.2 KiB
JavaScript

// Populates the database from data-pipeline/seed/manifest.json.
// Run after `npm run build:seed-models` and after backend Prisma migrations
// have been applied (this script uses plain SQL, not Prisma, so the
// data-pipeline stays independent of the backend's toolchain).
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
import { SYSTEMS as systems } from "../seed/systems.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestPath = path.resolve(__dirname, "../seed/manifest.json");
const { structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error("DATABASE_URL não definida.");
process.exit(1);
}
const client = new pg.Client({ connectionString });
await client.connect();
// The "api" service applies Prisma migrations on startup; if this pipeline
// runs first (e.g. right after `docker compose up`), wait for the table.
async function waitForTable(name, maxAttempts = 30) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const { rows } = await client.query(
`SELECT to_regclass($1) AS exists`,
[name],
);
if (rows[0]?.exists) return;
console.log(`Aguardando migrations do backend criarem "${name}" (${attempt}/${maxAttempts})...`);
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error(`Tabela "${name}" não apareceu — rode "docker compose up -d api" antes do pipeline.`);
}
await waitForTable("structures");
function cuid(prefix) {
return `${prefix}_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36)}`;
}
try {
await client.query("BEGIN");
const systemIds = new Map();
for (const s of systems) {
const id = cuid("sys");
systemIds.set(s.slug, id);
await client.query(
`INSERT INTO anatomical_systems (id, slug, "namePt", "nameEn", "colorHex", "order")
VALUES ($1,$2,$3,$4,$5,$6)
ON CONFLICT (slug) DO UPDATE SET "namePt"=$3, "nameEn"=$4, "colorHex"=$5, "order"=$6
RETURNING id`,
[id, s.slug, s.namePt, s.nameEn, s.colorHex, s.order],
);
}
// Re-read ids (in case of ON CONFLICT they may differ from freshly generated ones).
const { rows: existingSystems } = await client.query(
`SELECT id, slug FROM anatomical_systems`,
);
for (const row of existingSystems) systemIds.set(row.slug, row.id);
for (const st of structures) {
const systemId = systemIds.get(st.system);
if (!systemId) throw new Error(`Sistema desconhecido: ${st.system}`);
await client.query(
`INSERT INTO structures (id, "fmaId", slug, "namePt", "nameEn", synonyms, description, "regionPath", "modelFile", "meshName", "systemId", "updatedAt")
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11, now())
ON CONFLICT (slug) DO UPDATE SET
"namePt"=$4, "nameEn"=$5, synonyms=$6, description=$7, "regionPath"=$8,
"modelFile"=$9, "meshName"=$10, "systemId"=$11, "updatedAt"=now()`,
[
cuid("str"),
null,
st.slug,
st.namePt,
st.nameEn,
st.synonyms ?? [],
st.description ?? null,
st.regionPath ?? [],
st.modelFile,
st.meshName,
systemId,
],
);
}
// O manifesto de cada execução é sempre completo (cobre todos os
// sistemas) — qualquer coisa no banco cujo slug não está mais nele é
// obsoleta (ex: itens excluídos por um filtro/curadoria novos) e deve
// ser removida, senão elas só se acumulam a cada execução do pipeline.
const currentSlugs = structures.map((s) => s.slug);
await client.query(
`DELETE FROM quiz_cards WHERE "structureId" IN (
SELECT id FROM structures WHERE NOT (slug = ANY($1::text[]))
)`,
[currentSlugs],
);
const { rowCount: removed } = await client.query(
`DELETE FROM structures WHERE NOT (slug = ANY($1::text[]))`,
[currentSlugs],
);
await client.query("COMMIT");
console.log(
`Seed concluído: ${systems.length} sistemas, ${structures.length} estruturas (${removed} obsoletas removidas).`,
);
} catch (err) {
await client.query("ROLLBACK");
console.error(err);
process.exit(1);
} finally {
await client.end();
}