chore(db): harden workspace env wrappers
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user