import { createHash } from "node:crypto"; import process from "node:process"; import { createInterface } from "node:readline"; import pg from "pg"; const { Client } = pg; const EXPECTED_DATABASE = "booking_test"; const TARGET_SCHEMA = "condon"; const BASELINE_FINGERPRINT = // Read-only baseline captured from the existing booking/finance/ingestion/ // reporting schemas before the CONDO import. The CONDO migrations do not // target these schemas. "4b130b7f0f57eb80bb8b168fb1c11325db4c3b417e7c99cf423f9a48f0322af3"; const MIGRATION_VERSION = "001_create_condon_schema"; const MIGRATION_CHECKSUM = "7ab84c0c22fca16f75263be7291fc5c1977b46bada0794cf5b069e770d7e07a9"; const MIGRATION_V2 = "002_legacy_import_and_bookings"; const MIGRATION_V2_CHECKSUM = "cfc38024984c883490cb46ab8ab1fca2e72c67f10dc3a43c179b12a8e6f2405b"; const expectedColumns = { bookings: [ "confirmation_no", "created_at" ], entitlement_ledger: [ "id", "entitlement_period_id", "usage_record_id", "entry_type", "delta_nights", "balance_before", "balance_after", "occurred_at" ], entitlement_periods: [ "id", "owner_account_id", "period_year", "period_start", "period_end", "annual_grant", "carry_forward", "current_balance", "row_version", "created_at", "updated_at" ], owner_accounts: [ "id", "account_no", "transfer_date", "owner_name", "room_no", "purchased_room_type_code", "unit_no", "member_no", "created_at", "updated_at" ], room_types: [ "code", "entitlement_tier", "requires_manual_multiplier", "created_at", "updated_at" ], schema_migrations: [ "version", "checksum", "applied_at" ], usage_records: [ "id", "owner_account_id", "entitlement_period_id", "confirmation_no", "check_in", "check_out", "night_count", "used_room_type_code", "applied_multiplier", "rule_version", "use_nights", "balance_before", "balance_after", "remark", "idempotency_key", "created_at", "raw_used_room_type", "room_count", "source_sheet", "source_row", "source_sequence", "import_batch" ] }; const expectedIndexes = [ "bookings_created_idx", "bookings_pkey", "entitlement_ledger_period_occurred_idx", "entitlement_ledger_pkey", "entitlement_ledger_usage_record_key", "entitlement_periods_id_owner_key", "entitlement_periods_owner_year_key", "entitlement_periods_pkey", "owner_accounts_account_no_key", "owner_accounts_member_no_idx", "owner_accounts_pkey", "owner_accounts_purchased_room_type_idx", "owner_accounts_room_no_key", "room_types_pkey", "schema_migrations_pkey", "usage_records_id_period_key", "usage_records_idempotency_key_key", "usage_records_owner_created_idx", "usage_records_period_idx", "usage_records_pkey", "usage_records_source_location_key", "usage_records_used_type_check_in_idx" ].sort(); const expectedRoomTypes = [ ["AC2", null, true], ["RM1", 1, false], ["RM2", 1, false], ["RM3", 1, false], ["RM4", 1, false], ["SU1", 2, false], ["SU2", 2, false], ["SU3", 3, false], ["SU6", 2, false], ["UG1", 1, false], ["UG2", 1, false] ]; async function readStandardInput() { const lines = createInterface({ input: process.stdin, terminal: false }); for await (const line of lines) { lines.close(); return line.trim(); } throw Object.assign(new Error("Missing connection input"), { code: "MISSING_CONNECTION_INPUT" }); } function fingerprint(rows) { return createHash("sha256").update(JSON.stringify(rows)).digest("hex"); } function sameJson(left, right) { return JSON.stringify(left) === JSON.stringify(right); } function sanitizeError(error) { return { ok: false, errorCode: typeof error?.code === "string" ? error.code : "VERIFY_FAILED" }; } let client; let transactionStarted = false; try { const rawInput = await readStandardInput(); const input = JSON.parse(rawInput); client = new Client({ host: input.host, port: input.port, user: input.user, password: input.password, database: input.database, ssl: input.ssl ? { rejectUnauthorized: false } : undefined, application_name: "condon_readonly_verification", connectionTimeoutMillis: 10_000, query_timeout: 20_000, options: "-c default_transaction_read_only=on -c statement_timeout=20000 -c lock_timeout=2000" }); await client.connect(); await client.query("BEGIN TRANSACTION READ ONLY"); transactionStarted = true; const identityResult = await client.query(` SELECT current_database() AS database_name, current_user AS role_name, current_setting('transaction_read_only') AS transaction_read_only `); const identity = identityResult.rows[0]; const objectResult = await client.query(` SELECT n.nspname AS schema_name, c.relname AS object_name, c.relkind AS object_kind, pg_catalog.pg_get_userbyid(c.relowner) AS owner_name FROM pg_catalog.pg_class AS c JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace WHERE n.nspname NOT LIKE 'pg_%' AND n.nspname <> 'information_schema' AND n.nspname <> 'condon' ORDER BY n.nspname, c.relkind, c.relname `); const routineResult = await client.query(` SELECT n.nspname AS schema_name, p.proname AS routine_name, pg_catalog.pg_get_userbyid(p.proowner) AS owner_name, p.prokind AS routine_kind, pg_catalog.pg_get_function_identity_arguments(p.oid) AS identity_arguments FROM pg_catalog.pg_proc AS p JOIN pg_catalog.pg_namespace AS n ON n.oid = p.pronamespace WHERE n.nspname NOT LIKE 'pg_%' AND n.nspname <> 'information_schema' AND n.nspname <> 'condon' ORDER BY n.nspname, p.proname, identity_arguments `); const existingFingerprint = fingerprint({ objects: objectResult.rows, routines: routineResult.rows }); const columnResult = await client.query(` SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = 'condon' ORDER BY table_name, ordinal_position `); const actualColumns = columnResult.rows.reduce((tables, row) => { tables[row.table_name] ??= []; tables[row.table_name].push(row.column_name); return tables; }, {}); const indexResult = await client.query(` SELECT indexname AS index_name FROM pg_catalog.pg_indexes WHERE schemaname = 'condon' ORDER BY indexname `); const actualIndexes = indexResult.rows.map(row => row.index_name); const functionResult = await client.query(` SELECT p.proname AS function_name, p.prosecdef AS security_definer, p.provolatile AS volatility, p.proconfig @> ARRAY['search_path=pg_catalog']::text[] AS safe_search_path, p.proacl IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM pg_catalog.aclexplode(p.proacl) AS acl WHERE acl.grantee = 0 AND acl.privilege_type = 'EXECUTE' ) AS public_execute_revoked FROM pg_catalog.pg_proc AS p JOIN pg_catalog.pg_namespace AS n ON n.oid = p.pronamespace WHERE n.nspname = 'condon' ORDER BY p.proname `); const constraintResult = await client.query(` SELECT count(*)::integer AS constraint_count, COALESCE(bool_and( c.confrelid = 0 OR referenced_namespace.nspname = 'condon' ), true) AS all_foreign_keys_internal FROM pg_catalog.pg_constraint AS c JOIN pg_catalog.pg_namespace AS own_namespace ON own_namespace.oid = c.connamespace LEFT JOIN pg_catalog.pg_class AS referenced_class ON referenced_class.oid = c.confrelid LEFT JOIN pg_catalog.pg_namespace AS referenced_namespace ON referenced_namespace.oid = referenced_class.relnamespace WHERE own_namespace.nspname = 'condon' `); const aclResult = await client.query(` SELECT n.nspacl IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM pg_catalog.aclexplode(n.nspacl) AS acl WHERE acl.grantee = 0 ) AS schema_public_access_revoked, NOT EXISTS ( SELECT 1 FROM pg_catalog.pg_class AS c CROSS JOIN LATERAL pg_catalog.aclexplode( COALESCE(c.relacl, pg_catalog.acldefault('r', c.relowner)) ) AS acl WHERE c.relnamespace = n.oid AND acl.grantee = 0 ) AS tables_have_no_public_access FROM pg_catalog.pg_namespace AS n WHERE n.nspname = 'condon' `); const roomTypeResult = await client.query(` SELECT code, entitlement_tier, requires_manual_multiplier FROM condon.room_types ORDER BY code `); const actualRoomTypes = roomTypeResult.rows.map(row => [ row.code, row.entitlement_tier, row.requires_manual_multiplier ]); const countResult = await client.query(` SELECT (SELECT count(*)::integer FROM condon.owner_accounts) AS owner_account_count, (SELECT count(*)::integer FROM condon.entitlement_periods) AS entitlement_period_count, (SELECT count(*)::integer FROM condon.usage_records) AS usage_record_count, (SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger_count `); const businessCounts = countResult.rows[0]; const migrationResult = await client.query(` SELECT version, checksum FROM condon.schema_migrations `); const expectedFunctions = { calculate_multiplier: { volatility: "s" }, create_usage_record: { volatility: "v" }, create_usage_record_v2: { volatility: "v" }, ensure_booking_for_usage: { volatility: "v" }, open_entitlement_period: { volatility: "v" } }; const functionsValid = functionResult.rowCount === 5 && functionResult.rows.every(row => expectedFunctions[row.function_name]?.volatility === row.volatility && row.security_definer === false && row.safe_search_path === true && row.public_execute_revoked === true ); const checks = { databaseVerified: identity.database_name === EXPECTED_DATABASE && identity.role_name === input.user, transactionReadOnly: identity.transaction_read_only === "on", existingCatalogUnchanged: objectResult.rowCount === 128 && routineResult.rowCount === 5 && existingFingerprint === BASELINE_FINGERPRINT, exactTableColumns: sameJson(actualColumns, expectedColumns), exactIndexes: sameJson(actualIndexes, expectedIndexes), functionsValid, constraintsValid: constraintResult.rows[0].constraint_count === 49 && constraintResult.rows[0].all_foreign_keys_internal === true, permissionsRestricted: aclResult.rows[0]?.schema_public_access_revoked === true && aclResult.rows[0]?.tables_have_no_public_access === true, exactRoomTypes: sameJson(actualRoomTypes, expectedRoomTypes), businessDataImported: businessCounts.owner_account_count === 388 && businessCounts.entitlement_period_count === 389 && businessCounts.usage_record_count === 157 && businessCounts.ledger_count === 547, migrationVerified: migrationResult.rowCount === 2 && migrationResult.rows.some(row => row.version === MIGRATION_VERSION && row.checksum === MIGRATION_CHECKSUM ) && migrationResult.rows.some(row => row.version === MIGRATION_V2 && row.checksum === MIGRATION_V2_CHECKSUM ) }; process.stdout.write(`${JSON.stringify({ ok: Object.values(checks).every(Boolean), targetSchema: TARGET_SCHEMA, checks, existingCatalogFingerprint: existingFingerprint, targetSummary: { tableCount: Object.keys(actualColumns).length, indexCount: actualIndexes.length, constraintCount: constraintResult.rows[0].constraint_count, functionCount: functionResult.rowCount, roomTypeCount: roomTypeResult.rowCount, businessCounts } }, null, 2)}\n`); if (Object.values(checks).some(result => !result)) process.exitCode = 1; } catch (error) { process.stdout.write(`${JSON.stringify(sanitizeError(error), null, 2)}\n`); process.exitCode = 1; } finally { if (client) { if (transactionStarted) { try { await client.query("ROLLBACK"); } catch { process.exitCode = 1; } } await client.end().catch(() => { process.exitCode = 1; }); } }