chore(db): harden workspace env wrappers

This commit is contained in:
2026-03-31 22:47:07 +02:00
parent 5097ceab7e
commit 3e8b1702bc
6 changed files with 72 additions and 14 deletions
+6 -6
View File
@@ -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"
},
+33 -1
View File
@@ -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,
);
});
+2 -2
View File
@@ -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<typeof parseDatabaseUrl>) {
export function formatTarget(target: ReturnType<typeof parseDatabaseUrl>) {
const port = target.port ? `:${target.port}` : "";
return `${target.protocol}//${target.username}@${target.hostname}${port}/${target.databaseName}`;
}
+21 -1
View File
@@ -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;
}
+6 -2
View File
@@ -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");
+4 -2
View File
@@ -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);