#!/usr/bin/env node /** * Build the seeded template database, then clone it once per shard. * * node integration/scripts/prepare-shards.mjs # seed template + clone * node integration/scripts/prepare-shards.mjs --clone # clone only (per-run reset) * * Seeding has to happen here, not in vitest's globalSetup, because the order is * load-bearing and half of it is the app itself: * * 1. freight migrations (freight-migration-e2e container, before us) * 2. the app's always-on boot seeders — org, units, positions, permissions * (app.module.ts onApplicationBootstrap) * 3. the SQL fixtures, which declare those as prerequisites * (seed-users.sql: "created by the API's always-on boot seeders") * * Step 2 needs a real Nest boot, and the app now lives inside the vitest workers * — which globalSetup runs in a different process from. So we boot it once here * against the template and every shard is a `CREATE DATABASE … TEMPLATE` copy of * the result: a file copy on the tmpfs postgres, versus re-running migrations * and five seeders per shard. * * Run by it.mjs, which owns the env. No dependencies beyond `pg`. */ import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { Client } from "pg"; const itDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = resolve(itDir, ".."); const freightDir = join(repoRoot, "apps", "edr-freight-api"); const TEMPLATE = process.env.IT_TEMPLATE_DB ?? "edr_freight_e2e"; const PREFIX = process.env.IT_DB_PREFIX ?? "edr_it_s"; const SHARDS = Number(process.env.IT_SHARDS ?? 1); const admin = () => new Client({ host: process.env.DB_HOST ?? "localhost", port: Number(process.env.DB_PORT ?? 5543), user: process.env.DB_USER ?? "edr_e2e", password: process.env.DB_PASSWORD ?? "edr_e2e", // Never the template itself: CREATE DATABASE … TEMPLATE refuses while any // session is connected to the source. database: "postgres", }); const CYPRESS_FIXTURES = join(repoRoot, "e2e", "freight", "cypress", "fixtures"); const OWN_FIXTURES = join(itDir, "sql"); /** Order matters — company needs the users, everything needs the corridor. */ const SEEDS = [ [CYPRESS_FIXTURES, "seed-users.sql"], [CYPRESS_FIXTURES, "seed-company.sql"], [CYPRESS_FIXTURES, "seed-import-corridor.sql"], [CYPRESS_FIXTURES, "seed-bulk-items.sql"], [CYPRESS_FIXTURES, "seed-g1-train.sql"], [CYPRESS_FIXTURES, "seed-g2-weight.sql"], [CYPRESS_FIXTURES, "seed-government.sql"], [OWN_FIXTURES, "seed-company-b.sql"], [OWN_FIXTURES, "seed-customs-service-type.sql"], ]; async function seedTemplate() { console.log(`it: seeding template ${TEMPLATE}`); process.env.DB_NAME = TEMPLATE; // No broker for the template boot: payment.module.ts skips RabbitMQModule // entirely when this is unset, and nothing here publishes. delete process.env.PAYMENT_RABBITMQ_URL; const { createFreightApp } = createRequire(join(freightDir, "package.json"))("./dist/main.js"); // createFreightApp() only builds the graph. `init()` is what fires // onApplicationBootstrap — the seeders we are here for. We never listen. const app = await createFreightApp(); try { await app.init(); console.log("it: boot seeders done (org, units, positions, permissions)"); } finally { await app.close(); } const client = new Client({ host: process.env.DB_HOST ?? "localhost", port: Number(process.env.DB_PORT ?? 5543), user: process.env.DB_USER ?? "edr_e2e", password: process.env.DB_PASSWORD ?? "edr_e2e", database: TEMPLATE, }); await client.connect(); try { for (const [dir, file] of SEEDS) { await client.query(readFileSync(join(dir, file), "utf8")); console.log(`it: seeded ${file}`); } } finally { await client.end(); } } async function cloneShards() { const pg = admin(); await pg.connect(); try { // The app's pool and any stray psql hold the template open; without this the // CREATE below fails with "source database is being accessed by other users". await pg.query( `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [TEMPLATE], ); for (let i = 0; i < SHARDS; i++) { const db = `${PREFIX}${i}`; await pg.query( `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [db], ); await pg.query(`DROP DATABASE IF EXISTS ${db}`); await pg.query(`CREATE DATABASE ${db} TEMPLATE ${TEMPLATE}`); console.log(`it: shard ${i} → ${db}`); } } finally { await pg.end(); } } const cloneOnly = process.argv.includes("--clone"); if (!cloneOnly) await seedTemplate(); await cloneShards();