feat: add Ekklesia doctor command
CI / verify (pull_request) Successful in 20s
CI / infrastructure-smoke (pull_request) Successful in 20s
CI / migration-smoke (pull_request) Successful in 25s

This commit is contained in:
ekklesia
2026-07-21 10:42:21 -05:00
parent f89d575dd4
commit 02ee3ccd80
18 changed files with 1546 additions and 7 deletions
+72 -2
View File
@@ -1,4 +1,4 @@
import { Client, type QueryResultRow } from "pg";
import { Client, type ClientConfig, type QueryResultRow } from "pg";
import type {
AppliedMigrationRecord,
MigrationDatabase,
@@ -12,6 +12,7 @@ const historyTableName = "ekklesia_core_migrations";
const advisoryLockNamespace = 1_162_562_380;
const advisoryLockKey = 1_296_648_018;
const advisoryLockPollIntervalMs = 50;
const maximumPostgresTimeoutMs = 2_147_483_647;
const createHistoryTableSql = `
CREATE TABLE IF NOT EXISTS "public"."ekklesia_core_migrations" (
@@ -72,6 +73,11 @@ export class PostgresMigrationDatabaseError extends Error {
}
}
export interface PostgresMigrationDatabaseOptions {
readonly connectionTimeoutMs?: number;
readonly operationTimeoutMs?: number;
}
interface HistoryExistenceRow extends QueryResultRow {
readonly relation_kind: string;
}
@@ -124,6 +130,68 @@ function validateConnectionString(connectionString: string | undefined): string
return connectionString;
}
function invalidConfiguration(message: string): PostgresMigrationDatabaseError {
return databaseError("POSTGRES_MIGRATION_CONFIGURATION_INVALID", message);
}
function validateTimeout(value: unknown, message: string): number | undefined {
if (value === undefined) {
return undefined;
}
if (
typeof value !== "number" ||
!Number.isSafeInteger(value) ||
value <= 0 ||
value > maximumPostgresTimeoutMs
) {
throw invalidConfiguration(message);
}
return value;
}
function createClientConfig(
connectionString: string,
options: PostgresMigrationDatabaseOptions | undefined,
): ClientConfig {
if (
options !== undefined &&
(typeof options !== "object" || options === null || Array.isArray(options))
) {
throw invalidConfiguration("The PostgreSQL migration database options are invalid.");
}
let connectionTimeoutMs: unknown;
let operationTimeoutMs: unknown;
try {
connectionTimeoutMs = options?.connectionTimeoutMs;
operationTimeoutMs = options?.operationTimeoutMs;
} catch {
throw invalidConfiguration("The PostgreSQL migration database options are invalid.");
}
const validatedConnectionTimeoutMs = validateTimeout(
connectionTimeoutMs,
"The PostgreSQL connection timeout is invalid.",
);
const validatedOperationTimeoutMs = validateTimeout(
operationTimeoutMs,
"The PostgreSQL operation timeout is invalid.",
);
return {
connectionString,
...(validatedConnectionTimeoutMs === undefined
? {}
: { connectionTimeoutMillis: validatedConnectionTimeoutMs }),
...(validatedOperationTimeoutMs === undefined
? {}
: {
query_timeout: validatedOperationTimeoutMs,
statement_timeout: validatedOperationTimeoutMs,
}),
};
}
function isPostgresMigrationDatabaseError(error: unknown): error is PostgresMigrationDatabaseError {
return error instanceof PostgresMigrationDatabaseError;
}
@@ -440,12 +508,14 @@ class PostgresMigrationDatabaseSession implements MigrationDatabaseSession {
export function createPostgresMigrationDatabase(
connectionString: string | undefined,
options?: PostgresMigrationDatabaseOptions,
): MigrationDatabase {
const validatedConnectionString = validateConnectionString(connectionString);
const clientConfig = createClientConfig(validatedConnectionString, options);
return Object.freeze({
async openSession(): Promise<MigrationDatabaseSession> {
const client = new Client({ connectionString: validatedConnectionString });
const client = new Client(clientConfig);
client.on("error", () => undefined);
try {
await client.connect();