feat: implement metadata catalog database management

This commit is contained in:
Codex
2026-08-27 22:43:54 +02:00
parent 705af3aeb2
commit 79c4c925b5
86 changed files with 12566 additions and 135 deletions
+228
View File
@@ -0,0 +1,228 @@
import { buildInstallationContract } from "../workspaces/contracts.js";
import { resolveBinding } from "../workspaces/bindings.js";
import type { WorkspaceRegistry } from "../workspaces/registry.js";
import type { WorkspaceDescriptor } from "../workspaces/schema.js";
import type { WorkspaceSecretStore } from "../workspaces/secret-store.js";
import { createConcreteDiagnosticAdapters } from "../workspaces/diagnostics.js";
import { CatalogOperationCoordinator } from "./operation-coordinator.js";
import {
ConcreteCatalogPostgresAccess,
type CatalogPostgresAccess,
} from "./postgres-access.js";
import type {
CatalogRepository,
DatabaseBinding,
DatabaseConfigurationInput,
DatabaseTestResult,
WorkspaceDatabase,
} from "./types.js";
import { CATALOG_SECRET_IDS, type CatalogSecretName } from "./secrets.js";
export { CATALOG_SECRET_IDS, type CatalogSecretName } from "./secrets.js";
export interface CatalogListItem extends Omit<WorkspaceDatabase, "id"> {
id?: string;
workspaceName: string;
workspaceDescription?: string;
workspaceAvailable: boolean;
configured: boolean;
secrets: Record<CatalogSecretName, boolean>;
}
function bindingValue(workspace: WorkspaceDescriptor, values: Record<string, string>, suffix: string) {
const variable = buildInstallationContract(workspace).variables.find((entry) => (
entry.role === "DWH" && entry.suffix === suffix
));
return variable ? values[variable.name] : undefined;
}
function numeric(value: string | undefined): number | undefined {
if (!value) return undefined;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 ? parsed : undefined;
}
function yamlBinding(workspace: WorkspaceDescriptor, secretRoots: readonly string[]): DatabaseBinding {
const effective = resolveBinding(workspace, "DWH", process.env, secretRoots);
const value = (suffix: string) => bindingValue(workspace, effective.values, suffix);
return {
transport: effective.transport,
host: value("HOST"),
port: numeric(value("PORT")) ?? workspace.dwh.port,
username: value("USER"),
baseUrl: value("BASE_URL"),
restPath: workspace.diagnostics?.dwh_rest?.path ?? "/health",
restAuth: workspace.diagnostics?.dwh_rest?.auth ?? "bearer",
tlsServername: value("TLS_SERVERNAME"),
sshHost: value("SSH_HOST"),
sshPort: numeric(value("SSH_PORT")),
sshUsername: value("SSH_USER"),
sshTargetHost: value("SSH_TARGET_HOST"),
sshTargetPort: numeric(value("SSH_TARGET_PORT")),
};
}
function secretState(store: WorkspaceSecretStore, workspaceId: string): Record<CatalogSecretName, boolean> {
return Object.fromEntries(Object.entries(CATALOG_SECRET_IDS).map(([name, id]) => (
[name, store.has(workspaceId, id)]
))) as Record<CatalogSecretName, boolean>;
}
export class CatalogService {
private readonly adapters = createConcreteDiagnosticAdapters();
constructor(
private readonly repository: CatalogRepository,
private readonly registry: WorkspaceRegistry,
private readonly secretStore: WorkspaceSecretStore,
private readonly secretRoots: readonly string[],
private readonly diagnosticTimeoutMs: number,
private readonly postgres: CatalogPostgresAccess = new ConcreteCatalogPostgresAccess(secretStore, {
connectTimeoutMs: diagnosticTimeoutMs,
}),
private readonly operations: CatalogOperationCoordinator = new CatalogOperationCoordinator(),
) {}
async list(): Promise<CatalogListItem[]> {
const [workspaces, configured] = await Promise.all([
this.registry.listCatalog(),
this.repository.list(),
]);
const byWorkspace = new Map(configured.map((database) => [database.workspaceId, database]));
const active = await Promise.all(workspaces.map(async (entry) => {
const database = byWorkspace.get(entry.id);
const { workspace } = await this.registry.read(entry.id);
const base = database ?? {
workspaceId: entry.id,
engine: "postgres" as const,
databaseName: workspace.dwh.database,
schema: workspace.dwh.schema,
version: 0,
createdAt: "",
updatedAt: "",
binding: yamlBinding(workspace, this.secretRoots),
connectionStatus: "untested" as const,
};
return {
...base,
workspaceName: entry.name,
workspaceDescription: entry.description,
workspaceAvailable: true,
configured: database !== undefined,
secrets: secretState(this.secretStore, entry.id),
};
}));
const known = new Set(workspaces.map((entry) => entry.id));
const orphaned: CatalogListItem[] = configured
.filter((database) => !known.has(database.workspaceId))
.map((database) => ({
...database,
workspaceName: database.workspaceId,
workspaceDescription: "Workspace is no longer present in the repository catalog.",
workspaceAvailable: false,
configured: true,
secrets: secretState(this.secretStore, database.workspaceId),
}));
return [...active, ...orphaned];
}
async ensureWorkspace(workspaceId: string): Promise<WorkspaceDescriptor> {
const { workspace } = await this.registry.read(workspaceId);
return workspace;
}
async normalizeInput(input: DatabaseConfigurationInput): Promise<DatabaseConfigurationInput> {
const workspace = await this.ensureWorkspace(input.workspaceId);
if (input.binding.transport !== "rest_api") return input;
return {
...input,
binding: {
...input.binding,
restPath: workspace.diagnostics?.dwh_rest?.path ?? "/health",
},
};
}
configuredSecrets(workspaceId: string): Record<CatalogSecretName, boolean> {
return secretState(this.secretStore, workspaceId);
}
replaceSecrets(workspaceId: string, values: Partial<Record<CatalogSecretName, string>>): void {
const encoded: Record<string, string> = {};
for (const [name, value] of Object.entries(values) as Array<[CatalogSecretName, string | undefined]>) {
if (value !== undefined && value.length > 0) encoded[CATALOG_SECRET_IDS[name]] = value;
}
if (Object.keys(encoded).length > 0) this.secretStore.putMany(workspaceId, encoded);
}
forgetSecrets(workspaceId: string): void {
for (const id of Object.values(CATALOG_SECRET_IDS)) this.secretStore.forget(workspaceId, id);
}
async test(database: WorkspaceDatabase): Promise<DatabaseTestResult> {
return await this.operations.run(database.id, async () => {
const testedAt = new Date().toISOString();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.diagnosticTimeoutMs);
const required = database.binding.transport === "rest_api"
? database.binding.restAuth === "none" ? [] : [CATALOG_SECRET_IDS.apiKey]
: [];
const materialized = this.secretStore.materialize(database.workspaceId, required);
try {
if (database.binding.transport !== "rest_api") {
const client = await this.postgres.connect(database, controller.signal);
try {
const result = await client.query(
`SELECT current_database() AS database,
CASE WHEN pg_catalog.has_schema_privilege(
current_user,
(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = $1),
'USAGE'
) THEN $1 ELSE NULL END AS schema`,
[database.schema],
);
const row = result.rows[0];
if (row?.database !== database.databaseName || row.schema !== database.schema) {
throw new Error("Database identity mismatch");
}
} finally {
await client.end();
}
} else {
const credentialId = CATALOG_SECRET_IDS.apiKey;
await this.adapters.probeConnector({
role: "dwh",
transport: database.binding.transport,
baseUrl: database.binding.baseUrl,
credentialFile: materialized.files.get(credentialId),
resource: { database: database.databaseName, schema: database.schema },
timeoutMs: this.diagnosticTimeoutMs,
signal: controller.signal,
diagnostic: {
method: "GET" as const,
path: database.binding.restPath ?? "/health",
auth: database.binding.restAuth ?? "bearer",
},
});
}
return {
connectionStatus: "reachable",
testedVersion: database.version,
lastTestedAt: testedAt,
};
} catch {
return {
connectionStatus: "failed",
testedVersion: database.version,
lastTestedAt: testedAt,
errorCode: "connector_unavailable",
errorMessage: "The database connector could not be reached or authenticated.",
};
} finally {
clearTimeout(timer);
controller.abort();
materialized.release();
}
});
}
}