// 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(); }