Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/adopt-prisma-baseline.mjs
T

511 lines
20 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import pg from "pg";
export const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "..");
export const BASELINE_MANIFEST_PATH = path.join(REPOSITORY_ROOT, "prisma/baseline-legacy-manifest.json");
export const LEGACY_MIGRATION_ARCHIVE_PATH = path.join(REPOSITORY_ROOT, "prisma/baseline-legacy-migrations.tar.gz");
export const PRISMA_CLI_PATH = path.join(REPOSITORY_ROOT, "node_modules/prisma/build/index.js");
const IMPERSONATION_AUDIT_MIGRATION = "20260508140000_impersonation_audit";
const BOOKING_OVERLAP_MIGRATION = "20260717090000_ak_p16b_booking_overlap_constraint";
const TENANT_DELETION_MIGRATION = "20260721090000_ak_p21_tenant_deletion_capabilities";
const SUPPORT_RETENTION_MIGRATION = "20260721093000_ak_p21_support_retention_purge";
export const MIGRATION_TABLE_EXISTS_QUERY = `
SELECT to_regclass('public._prisma_migrations') IS NOT NULL AS "tableExists";
`;
export const MIGRATION_HISTORY_QUERY = `
SELECT
migration_name AS "name",
checksum,
finished_at AS "finishedAt",
rolled_back_at AS "rolledBackAt"
FROM public._prisma_migrations
ORDER BY started_at, id;
`;
export const LEGACY_VARIANT_CATALOG_CHECKS = [
{
id: "impersonation audit delete semantics",
migrations: [IMPERSONATION_AUDIT_MIGRATION],
query: `
SELECT
(SELECT is_nullable = 'YES'
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'ImpersonationAudit' AND column_name = 'actorUserId')
AND
(SELECT is_nullable = 'YES'
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'ImpersonationAudit' AND column_name = 'targetUserId')
AND
(SELECT confdeltype = 'n'
FROM pg_constraint
WHERE conrelid = 'public."ImpersonationAudit"'::regclass AND conname = 'ImpersonationAudit_actorUserId_fkey')
AND
(SELECT confdeltype = 'n'
FROM pg_constraint
WHERE conrelid = 'public."ImpersonationAudit"'::regclass AND conname = 'ImpersonationAudit_targetUserId_fkey')
AS "matches";
`,
},
{
id: "tenant-scoped booking overlap constraint",
migrations: [BOOKING_OVERLAP_MIGRATION],
query: `
SELECT COALESCE(
(SELECT pg_get_constraintdef(oid, false) LIKE '%"tenantId" WITH =%'
FROM pg_constraint
WHERE conrelid = 'public."Appointment"'::regclass AND conname = 'appointment_no_overlap'),
false
) AS "matches";
`,
},
{
id: "tenant deletion function ownership and ACLs",
migrations: [TENANT_DELETION_MIGRATION],
query: `
WITH expected(function_oid) AS (
VALUES
('public.hivegrid_purge_tenant_guarded_rows(text)'::regprocedure::oid),
('public.hivegrid_minimize_tenant_support_pii(text)'::regprocedure::oid)
)
SELECT COUNT(*) = 2
AND BOOL_AND(pg_get_userbyid(proc.proowner) = 'hivegrid_rls_definer')
AND BOOL_AND(has_function_privilege('hivegrid_worker', proc.oid, 'EXECUTE'))
AND BOOL_AND(NOT has_function_privilege('hivegrid_app', proc.oid, 'EXECUTE'))
AND BOOL_AND(NOT has_function_privilege('hivegrid_authentik_jit', proc.oid, 'EXECUTE'))
AND BOOL_AND(NOT EXISTS (
SELECT 1
FROM aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) AS acl
WHERE acl.grantee = 0 AND acl.privilege_type = 'EXECUTE'
)) AS "matches"
FROM expected
JOIN pg_proc AS proc ON proc.oid = expected.function_oid;
`,
},
{
id: "support retention function ownership and ACLs",
migrations: [SUPPORT_RETENTION_MIGRATION],
query: `
SELECT pg_get_userbyid(proc.proowner) = 'hivegrid_rls_definer'
AND has_function_privilege('hivegrid_worker', proc.oid, 'EXECUTE')
AND NOT has_function_privilege('hivegrid_app', proc.oid, 'EXECUTE')
AND NOT has_function_privilege('hivegrid_authentik_jit', proc.oid, 'EXECUTE')
AND NOT EXISTS (
SELECT 1
FROM aclexplode(COALESCE(proc.proacl, acldefault('f', proc.proowner))) AS acl
WHERE acl.grantee = 0 AND acl.privilege_type = 'EXECUTE'
) AS "matches"
FROM pg_proc AS proc
WHERE proc.oid = 'public.hivegrid_purge_tenant_retained_support(text)'::regprocedure;
`,
},
];
export class BaselineAdoptionError extends Error {
constructor(message, options) {
super(message, options);
this.name = "BaselineAdoptionError";
}
}
export function readBaselineManifest(manifestPath = BASELINE_MANIFEST_PATH) {
return JSON.parse(readFileSync(manifestPath, "utf8"));
}
function assertManifest(manifest) {
if (
manifest?.version !== 1 ||
typeof manifest.baselineMigration !== "string" ||
!/^[a-f0-9]{64}$/.test(manifest.baselineChecksum ?? "") ||
!Array.isArray(manifest.migrations) ||
manifest.legacyMigrationCount !== manifest.migrations.length
) {
throw new BaselineAdoptionError("The Prisma baseline legacy manifest is malformed");
}
const names = new Set();
for (const migration of manifest.migrations) {
if (typeof migration?.name !== "string" || !/^[a-f0-9]{64}$/.test(migration.checksum ?? "") || names.has(migration.name)) {
throw new BaselineAdoptionError("The Prisma baseline legacy manifest contains an invalid or duplicate migration");
}
names.add(migration.name);
}
if (!Array.isArray(manifest.legacyChecksumVariants)) {
throw new BaselineAdoptionError("The Prisma baseline legacy manifest is missing its reviewed checksum variants");
}
const currentChecksums = new Map(manifest.migrations.map((migration) => [migration.name, migration.checksum]));
const variantKeys = new Set();
for (const variant of manifest.legacyChecksumVariants) {
const key = `${variant?.name}:${variant?.checksum}`;
if (
!names.has(variant?.name) ||
!/^[a-f0-9]{64}$/.test(variant?.checksum ?? "") ||
!/^[a-f0-9]{40}$/.test(variant?.sourceCommit ?? "") ||
currentChecksums.get(variant.name) === variant.checksum ||
variantKeys.has(key)
) {
throw new BaselineAdoptionError("The Prisma baseline legacy manifest contains an invalid checksum variant");
}
variantKeys.add(key);
}
}
function isSuccessful(row) {
return row.finishedAt != null && row.rolledBackAt == null;
}
function isUnresolvedFailure(row) {
return row.finishedAt == null && row.rolledBackAt == null;
}
function matchesReviewedChecksum(name, checksum, expectedChecksum, reviewedVariants) {
return checksum === expectedChecksum || reviewedVariants.get(name)?.has(checksum);
}
export function classifyMigrationHistory({ tableExists, rows }, manifest) {
assertManifest(manifest);
if (!tableExists) return { mode: "fresh", detail: "migration table absent" };
if (!Array.isArray(rows)) throw new BaselineAdoptionError("Prisma migration history rows are unavailable");
if (rows.some(isUnresolvedFailure)) {
throw new BaselineAdoptionError("Prisma migration history contains an unresolved failed migration");
}
const successful = rows.filter(isSuccessful);
const baselineRows = successful.filter((row) => row.name === manifest.baselineMigration);
if (baselineRows.length > 0) {
if (baselineRows.length !== 1 || baselineRows[0].checksum !== manifest.baselineChecksum) {
throw new BaselineAdoptionError("The applied Prisma baseline does not match the committed baseline checksum");
}
return { mode: "baseline", detail: "baseline already applied" };
}
if (rows.length === 0) return { mode: "fresh", detail: "migration history empty" };
const expected = new Map(manifest.migrations.map((migration) => [migration.name, migration.checksum]));
const reviewedVariants = new Map();
for (const variant of manifest.legacyChecksumVariants) {
const checksums = reviewedVariants.get(variant.name) ?? new Set();
checksums.add(variant.checksum);
reviewedVariants.set(variant.name, checksums);
}
const rowNames = new Set(rows.map((row) => row.name));
const unexpectedNames = [...rowNames].filter((name) => !expected.has(name));
if (unexpectedNames.length > 0) {
throw new BaselineAdoptionError("Prisma migration history contains entries outside the reviewed legacy chain");
}
const successfulByName = new Map();
for (const row of successful) {
const entries = successfulByName.get(row.name) ?? [];
entries.push(row);
successfulByName.set(row.name, entries);
}
const variantNames = [];
let successfulPrefixLength = 0;
let gapFound = false;
for (const [name, checksum] of expected) {
const entries = successfulByName.get(name) ?? [];
if (entries.length === 0) {
gapFound = true;
continue;
}
if (entries.length !== 1 || gapFound) {
throw new BaselineAdoptionError("Prisma migration history is not an exact legacy prefix");
}
if (!matchesReviewedChecksum(name, entries[0].checksum, checksum, reviewedVariants)) {
throw new BaselineAdoptionError(`Prisma migration history checksum differs from the reviewed legacy chain (${name})`);
}
if (entries[0].checksum !== checksum) {
variantNames.push(name);
}
successfulPrefixLength += 1;
}
if (successful.length !== successfulPrefixLength) {
throw new BaselineAdoptionError("Prisma migration history contains duplicate successful legacy entries");
}
if (successfulPrefixLength < manifest.migrations.length) {
return {
mode: "legacy-prefix",
detail: "checksum-verified legacy prefix",
variantNames,
appliedMigrationCount: successfulPrefixLength,
missingMigrations: manifest.migrations.slice(successfulPrefixLength).map((migration) => migration.name),
};
}
if (successful.length !== manifest.migrations.length) {
throw new BaselineAdoptionError("Prisma migration history has an unexpected successful legacy migration count");
}
return {
mode: "legacy",
detail: variantNames.length === 0 ? "complete reviewed legacy chain" : "complete reviewed legacy chain with historical checksums",
variantNames,
};
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
export function prepareLegacyMigrationArchive({ manifest, archivePath = LEGACY_MIGRATION_ARCHIVE_PATH, spawnImpl = spawnSync, tempRoot = os.tmpdir() } = {}) {
assertManifest(manifest);
if (!/^[a-f0-9]{64}$/.test(manifest.legacyArchiveChecksum ?? "") || sha256(readFileSync(archivePath)) !== manifest.legacyArchiveChecksum) {
throw new BaselineAdoptionError("The Prisma legacy migration archive does not match the reviewed manifest");
}
const temporaryDirectory = mkdtempSync(path.join(tempRoot, "hivegrid-legacy-catch-up-"));
const cleanup = () => rmSync(temporaryDirectory, { recursive: true, force: true });
try {
const extraction = spawnImpl("tar", ["-xzf", archivePath, "-C", temporaryDirectory], {
cwd: REPOSITORY_ROOT,
stdio: "inherit",
});
if (extraction.error || extraction.status !== 0) {
throw new BaselineAdoptionError("Could not extract the reviewed Prisma legacy migration archive", {
cause: extraction.error,
});
}
const migrationsPath = path.join(temporaryDirectory, "prisma/migrations");
const migrationNames = readdirSync(migrationsPath, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
const expectedNames = manifest.migrations.map((migration) => migration.name);
if (JSON.stringify(migrationNames) !== JSON.stringify(expectedNames)) {
throw new BaselineAdoptionError("The Prisma legacy migration archive contains an unexpected migration set");
}
for (const migration of manifest.migrations) {
const sqlPath = path.join(migrationsPath, migration.name, "migration.sql");
if (sha256(readFileSync(sqlPath)) !== migration.checksum) {
throw new BaselineAdoptionError(`The Prisma legacy migration archive checksum differs (${migration.name})`);
}
}
return { migrationsPath, cleanup };
} catch (error) {
cleanup();
throw error;
}
}
export function deployMissingLegacyMigrations({
manifest,
missingMigrations,
env = process.env,
prepareArchive = prepareLegacyMigrationArchive,
spawnImpl = spawnSync,
tempRoot = os.tmpdir(),
} = {}) {
const connectionString = env.DIRECT_URL?.trim();
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma legacy catch-up");
const expectedMissing = manifest.migrations.slice(manifest.migrations.length - missingMigrations.length).map((migration) => migration.name);
if (missingMigrations.length === 0 || JSON.stringify(missingMigrations) !== JSON.stringify(expectedMissing)) {
throw new BaselineAdoptionError("Prisma legacy catch-up requires the exact missing manifest suffix");
}
const prepared = prepareArchive({ manifest });
let configDirectory;
try {
configDirectory = mkdtempSync(path.join(tempRoot, "hivegrid-prisma-config-"));
const configPath = path.join(configDirectory, "prisma.config.ts");
const prismaConfigUrl = pathToFileURL(path.join(REPOSITORY_ROOT, "node_modules/prisma/config.js")).href;
writeFileSync(
configPath,
[
`import { defineConfig, env } from ${JSON.stringify(prismaConfigUrl)};`,
"",
"export default defineConfig({",
` schema: ${JSON.stringify(path.join(REPOSITORY_ROOT, "prisma/schema"))},`,
` migrations: { path: ${JSON.stringify(prepared.migrationsPath)} },`,
' datasource: { url: env("DIRECT_URL") },',
"});",
"",
].join("\n"),
{ encoding: "utf8", mode: 0o600 }
);
const result = spawnImpl(process.execPath, [PRISMA_CLI_PATH, "migrate", "deploy", "--config", configPath], {
cwd: REPOSITORY_ROOT,
env,
stdio: "inherit",
});
if (result.error || result.status !== 0) {
throw new BaselineAdoptionError("Prisma could not apply the missing reviewed legacy migration suffix", {
cause: result.error,
});
}
} finally {
if (configDirectory) rmSync(configDirectory, { recursive: true, force: true });
prepared.cleanup();
}
}
export function verifyPrismaSchemaParity({ env = process.env, spawnImpl = spawnSync } = {}) {
const result = spawnImpl(
process.execPath,
[PRISMA_CLI_PATH, "migrate", "diff", "--from-config-datasource", "--to-schema", "prisma/schema", "--exit-code", "--config", "prisma.config.ts"],
{
cwd: REPOSITORY_ROOT,
env,
stdio: "inherit",
}
);
if (result.error || result.status === 1 || result.status == null) {
throw new BaselineAdoptionError("Could not verify the live database schema after applying migrations", {
cause: result.error,
});
}
if (result.status === 2) {
throw new BaselineAdoptionError("Live database schema differs from the reviewed current Prisma schema after applying migrations");
}
if (result.status !== 0) {
throw new BaselineAdoptionError(`Live database schema verification returned unsupported exit status ${result.status}`);
}
}
export async function verifyLegacyVariantCatalog({
variantNames,
env = process.env,
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-prisma-baseline-variant-verification" }),
} = {}) {
const requiredChecks = LEGACY_VARIANT_CATALOG_CHECKS.filter((check) => check.migrations.some((migration) => variantNames.includes(migration)));
if (requiredChecks.length === 0) return;
const connectionString = env.DIRECT_URL?.trim();
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma baseline variant verification");
const client = clientFactory(connectionString);
try {
await client.connect();
for (const check of requiredChecks) {
const result = await client.query(check.query);
if (result.rows[0]?.matches !== true) {
throw new BaselineAdoptionError(`Live database does not satisfy the reviewed ${check.id} contract`);
}
}
} catch (error) {
if (error instanceof BaselineAdoptionError) throw error;
throw new BaselineAdoptionError("Could not verify the live database catalog before adopting historical checksums", {
cause: error,
});
} finally {
await client.end().catch(() => undefined);
}
}
export async function inspectMigrationHistory({
env = process.env,
clientFactory = (connectionString) => new pg.Client({ connectionString, application_name: "hivegrid-prisma-baseline-adoption" }),
} = {}) {
const connectionString = env.DIRECT_URL?.trim();
if (!connectionString) throw new BaselineAdoptionError("DIRECT_URL is required for Prisma baseline adoption");
const client = clientFactory(connectionString);
try {
await client.connect();
const tableState = await client.query(MIGRATION_TABLE_EXISTS_QUERY);
const tableExists = tableState.rows[0]?.tableExists === true;
if (!tableExists) return { tableExists: false, rows: [] };
const history = await client.query(MIGRATION_HISTORY_QUERY);
return { tableExists: true, rows: history.rows };
} catch (error) {
if (error instanceof BaselineAdoptionError) throw error;
throw new BaselineAdoptionError("Could not inspect Prisma migration history; inspect restricted database logs", { cause: error });
} finally {
await client.end().catch(() => undefined);
}
}
export function resolveBaselineMigration({ manifest, env = process.env, spawnImpl = spawnSync } = {}) {
const result = spawnImpl(process.execPath, [PRISMA_CLI_PATH, "migrate", "resolve", "--applied", manifest.baselineMigration, "--config", "prisma.config.ts"], {
cwd: REPOSITORY_ROOT,
env,
stdio: "inherit",
});
if (result.error || result.status !== 0) {
throw new BaselineAdoptionError("Prisma could not record the reviewed baseline migration", { cause: result.error });
}
}
export async function adoptPrismaBaseline({
manifest = readBaselineManifest(),
inspect = inspectMigrationHistory,
deployMissing = deployMissingLegacyMigrations,
verifyCatalog = verifyLegacyVariantCatalog,
resolve = resolveBaselineMigration,
stdout = process.stdout,
} = {}) {
let before = classifyMigrationHistory(await inspect(), manifest);
if (before.mode === "fresh") {
stdout.write("[baseline] fresh database; migrate deploy will apply the baseline\n");
return before;
}
if (before.mode === "baseline") {
stdout.write("[baseline] committed baseline already recorded\n");
return before;
}
if (before.mode === "legacy-prefix") {
stdout.write(
`[baseline] applying ${before.missingMigrations.length} checksum-verified legacy migration(s) after ${before.appliedMigrationCount} applied entries\n`
);
await deployMissing({ manifest, missingMigrations: before.missingMigrations });
before = classifyMigrationHistory(await inspect(), manifest);
if (before.mode !== "legacy") {
throw new BaselineAdoptionError("Prisma legacy catch-up did not produce the complete reviewed legacy chain");
}
stdout.write("[baseline] legacy migration suffix applied and verified\n");
}
if (before.variantNames.length > 0) {
stdout.write(`[baseline] verifying ${before.variantNames.length} reviewed historical checksum variant catalog contract(s)\n`);
await verifyCatalog({ variantNames: before.variantNames });
stdout.write("[baseline] security-sensitive historical catalog contracts verified\n");
}
stdout.write(`[baseline] adopting ${manifest.baselineMigration} over the checksum-verified legacy chain\n`);
await resolve({ manifest });
const after = classifyMigrationHistory(await inspect(), manifest);
if (after.mode !== "baseline") {
throw new BaselineAdoptionError("Prisma baseline adoption did not produce a verified baseline history entry");
}
stdout.write("[baseline] adoption verified\n");
return { mode: "adopted", detail: before.detail };
}
const isCli = process.argv[1] && path.join(import.meta.dirname, "adopt-prisma-baseline.mjs") === path.resolve(process.argv[1]);
if (isCli) {
try {
const args = process.argv.slice(2);
if (args.length === 0 || (args.length === 1 && args[0] === "--adopt")) {
await adoptPrismaBaseline();
} else if (args.length === 1 && args[0] === "--verify-current-schema") {
verifyPrismaSchemaParity();
process.stdout.write("[baseline] current live schema verified after migrations\n");
} else {
throw new BaselineAdoptionError("Unsupported Prisma baseline adoption argument");
}
} catch (error) {
const message = error instanceof BaselineAdoptionError ? error.message : "Prisma baseline adoption failed without a safe diagnostic";
process.stderr.write(`FAIL ${message}\n`);
process.exitCode = 1;
}
}