diff --git a/packages/db/package.json b/packages/db/package.json index f86f958..6a0958a 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,10 +9,10 @@ }, "scripts": { "db:doctor": "node ../../scripts/db-doctor.mjs capakraken", - "db:push": "node ../../scripts/with-env.mjs prisma db push --schema ./prisma/schema.prisma", - "db:migrate": "node ../../scripts/with-env.mjs prisma migrate dev --schema ./prisma/schema.prisma", - "db:migrate:deploy": "node ../../scripts/with-env.mjs prisma migrate deploy --schema ./prisma/schema.prisma", - "db:validate": "node ../../scripts/with-env.mjs prisma validate --schema ./prisma/schema.prisma", + "db:push": "node ../../scripts/prisma-with-env.mjs db push --schema ./prisma/schema.prisma", + "db:migrate": "node ../../scripts/prisma-with-env.mjs migrate dev --schema ./prisma/schema.prisma", + "db:migrate:deploy": "node ../../scripts/prisma-with-env.mjs migrate deploy --schema ./prisma/schema.prisma", + "db:validate": "node ../../scripts/prisma-with-env.mjs validate --schema ./prisma/schema.prisma", "db:seed": "node ../../scripts/with-env.mjs tsx src/seed.ts", "db:seed:holiday-demo-resources": "node ../../scripts/with-env.mjs tsx src/seed-holiday-demo-resources.ts", "db:seed:holidays": "node ../../scripts/with-env.mjs tsx src/seed-holiday-calendars.ts", @@ -21,8 +21,8 @@ "db:reset:dispo": "node ../../scripts/with-env.mjs tsx src/reset-dispo-import.ts", "db:import:dispo": "node ../../scripts/with-env.mjs tsx src/import-dispo-batch.ts", "db:excel": "node ../../scripts/with-env.mjs tsx src/generate-excel.ts", - "db:studio": "node ../../scripts/with-env.mjs prisma studio --schema ./prisma/schema.prisma", - "db:generate": "node ../../scripts/with-env.mjs prisma generate --schema ./prisma/schema.prisma", + "db:studio": "node ../../scripts/prisma-with-env.mjs studio --schema ./prisma/schema.prisma", + "db:generate": "node ../../scripts/prisma-with-env.mjs generate --schema ./prisma/schema.prisma", "test:unit": "tsx --test src/*.test.ts", "typecheck": "tsc --noEmit" }, diff --git a/packages/db/src/destructive-db-guard.test.ts b/packages/db/src/destructive-db-guard.test.ts index 839421b..2a1501a 100644 --- a/packages/db/src/destructive-db-guard.test.ts +++ b/packages/db/src/destructive-db-guard.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { assertDestructiveDbAllowed } from "./destructive-db-guard.js"; -import { assertSafeSeedTarget } from "./safe-destructive-env.js"; +import { assertCapaKrakenDbTarget, assertSafeSeedTarget } from "./safe-destructive-env.js"; const ORIGINAL_ENV = { ...process.env }; @@ -98,3 +98,35 @@ test("assertSafeSeedTarget rejects unexpected legacy disposable databases", () = /not in the destructive-tool allowlist/u, ); }); + +test("assertCapaKrakenDbTarget accepts non-destructive capakraken targets", () => { + setEnv({ + DATABASE_URL: "postgresql://tester:secret@localhost:5432/capakraken_dev", + }); + + const target = assertCapaKrakenDbTarget("db:seed:holidays"); + + assert.equal(target.databaseName, "capakraken_dev"); +}); + +test("assertCapaKrakenDbTarget rejects legacy non-capakraken targets", () => { + setEnv({ + DATABASE_URL: "postgresql://tester:secret@localhost:5432/legacy_non_capakraken", + }); + + assert.throws( + () => assertCapaKrakenDbTarget("db:seed:holidays"), + /not a valid CapaKraken target/u, + ); +}); + +test("assertCapaKrakenDbTarget explains missing env loading clearly", () => { + setEnv({ + DATABASE_URL: undefined, + }); + + assert.throws( + () => assertCapaKrakenDbTarget("db:update:blueprints"), + /Run the command through the CapaKraken env wrappers/u, + ); +}); diff --git a/packages/db/src/destructive-db-guard.ts b/packages/db/src/destructive-db-guard.ts index b77c77d..bc0f923 100644 --- a/packages/db/src/destructive-db-guard.ts +++ b/packages/db/src/destructive-db-guard.ts @@ -8,7 +8,7 @@ interface DestructiveGuardOptions { const PROTECTED_DATABASE_NAMES = new Set(["capakraken"]); -function parseDatabaseUrl(rawUrl: string) { +export function parseDatabaseUrl(rawUrl: string) { const parsed = new URL(rawUrl); const databaseName = parsed.pathname.replace(/^\/+/, ""); @@ -21,7 +21,7 @@ function parseDatabaseUrl(rawUrl: string) { }; } -function formatTarget(target: ReturnType) { +export function formatTarget(target: ReturnType) { const port = target.port ? `:${target.port}` : ""; return `${target.protocol}//${target.username}@${target.hostname}${port}/${target.databaseName}`; } diff --git a/packages/db/src/safe-destructive-env.ts b/packages/db/src/safe-destructive-env.ts index 3e6db0e..860f68e 100644 --- a/packages/db/src/safe-destructive-env.ts +++ b/packages/db/src/safe-destructive-env.ts @@ -1,4 +1,4 @@ -import { assertDestructiveDbAllowed } from "./destructive-db-guard.js"; +import { assertDestructiveDbAllowed, formatTarget, parseDatabaseUrl } from "./destructive-db-guard.js"; const TEST_DATABASE_NAMES = [ "capakraken_test", @@ -12,3 +12,23 @@ export function assertSafeSeedTarget(commandName: string) { allowedDatabaseNames: TEST_DATABASE_NAMES, }); } + +export function assertCapaKrakenDbTarget(commandName: string) { + const rawUrl = process.env.DATABASE_URL; + + if (!rawUrl) { + throw new Error( + `${commandName} aborted: DATABASE_URL is not configured. Run the command through the CapaKraken env wrappers so the workspace env files are loaded.`, + ); + } + + const target = parseDatabaseUrl(rawUrl); + + if (!target.databaseName.startsWith("capakraken")) { + throw new Error( + `${commandName} aborted: database '${target.databaseName}' is not a valid CapaKraken target. Target=${formatTarget(target)}`, + ); + } + + return target; +} diff --git a/scripts/load-env.mjs b/scripts/load-env.mjs index eff4693..933f0d7 100644 --- a/scripts/load-env.mjs +++ b/scripts/load-env.mjs @@ -1,11 +1,15 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -function resolveWorkspaceRoot() { +export function resolveWorkspaceRoot() { return resolve(dirname(fileURLToPath(import.meta.url)), ".."); } +export function resolveRealWorkspaceRoot() { + return realpathSync(resolveWorkspaceRoot()); +} + export function resolveWorkspaceEnvPath(options = {}) { const { workspaceRoot = resolveWorkspaceRoot() } = options; return resolve(workspaceRoot, ".env"); diff --git a/scripts/with-env.mjs b/scripts/with-env.mjs index beebf6f..835f661 100644 --- a/scripts/with-env.mjs +++ b/scripts/with-env.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import { loadWorkspaceEnv } from "./load-env.mjs"; +import { loadWorkspaceEnv, resolveRealWorkspaceRoot } from "./load-env.mjs"; loadWorkspaceEnv(); +const workspaceRoot = resolveRealWorkspaceRoot(); +process.chdir(workspaceRoot); const args = process.argv.slice(2); @@ -15,6 +17,7 @@ if (args.length === 0) { const result = spawnSync(args[0], args.slice(1), { stdio: "inherit", env: process.env, + cwd: workspaceRoot, }); if (result.error) { @@ -23,4 +26,3 @@ if (result.error) { } process.exit(result.status ?? 1); -