Separate infrastructure to separate yaml and base schema.

This commit is contained in:
Patrick Fic
2026-01-14 14:52:49 -08:00
parent 66fcaaf8f4
commit fefbd45570
8 changed files with 847 additions and 451 deletions

View File

@@ -7,17 +7,88 @@ import { getDb } from '../lib/db';
export const handler = async (): Promise<APIGatewayProxyResult> => {
try {
const db = await getDb();
const result = await db.execute(sql`select 1 as ok`);
const ping = await db.execute(sql`select 1 as ok`);
const schemaRows = await db.execute(sql`
select
table_schema,
table_name,
column_name,
data_type,
is_nullable,
ordinal_position
from information_schema.columns
where table_schema not in ('pg_catalog', 'information_schema')
order by table_schema, table_name, ordinal_position
`);
type ColumnInfo = {
name: string;
type: string;
nullable: boolean;
position: number;
};
type TableInfo = {
schema: string;
name: string;
columns: ColumnInfo[];
};
const tableMap = new Map<string, TableInfo>();
const rows = (schemaRows as unknown as { rows?: unknown[] })?.rows ?? [];
for (const row of rows as Array<Record<string, unknown>>) {
const tableSchema = String(row.table_schema ?? '');
const tableName = String(row.table_name ?? '');
if (!tableSchema || !tableName) continue;
const key = `${tableSchema}.${tableName}`;
const existing = tableMap.get(key);
const tableInfo: TableInfo = existing ?? {
schema: tableSchema,
name: tableName,
columns: [],
};
tableInfo.columns.push({
name: String(row.column_name ?? ''),
type: String(row.data_type ?? ''),
nullable: String(row.is_nullable ?? '') === 'YES',
position: Number(row.ordinal_position ?? 0),
});
if (!existing) tableMap.set(key, tableInfo);
}
const tables = [...tableMap.values()];
return {
statusCode: 200,
body: JSON.stringify({ success: true, result }),
body: JSON.stringify({ success: true, ping, schema: { tables } }),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const maybeAny = error as unknown as { cause?: unknown; code?: unknown };
const causeMessage =
maybeAny?.cause instanceof Error
? maybeAny.cause.message
: typeof maybeAny?.cause === 'string'
? maybeAny.cause
: undefined;
const causeCode =
typeof (maybeAny?.cause as { code?: unknown } | undefined)?.code === 'string'
? (maybeAny.cause as { code?: string }).code
: typeof maybeAny?.code === 'string'
? maybeAny.code
: undefined;
return {
statusCode: 500,
body: JSON.stringify({ success: false, error: errorMessage }),
body: JSON.stringify({
success: false,
error: errorMessage,
cause: causeMessage,
code: causeCode,
}),
};
}
};

View File

@@ -5,8 +5,8 @@ import { Pool } from 'pg';
import * as schema from '../db/schema';
type DbSecret = {
username: string;
password: string;
username: string;
password: string;
};
let cachedSecret: DbSecret | undefined;
@@ -14,62 +14,65 @@ let cachedPool: Pool | undefined;
let cachedDb: NodePgDatabase<typeof schema> | undefined;
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
async function getDbSecret(): Promise<DbSecret> {
if (cachedSecret) return cachedSecret;
if (cachedSecret) return cachedSecret;
const secretArn = requireEnv('DB_SECRET_ARN');
const client = new SecretsManagerClient({});
const result = await client.send(
new GetSecretValueCommand({
SecretId: secretArn,
}),
);
const secretArn = requireEnv('DB_SECRET_ARN');
const client = new SecretsManagerClient({});
const result = await client.send(
new GetSecretValueCommand({
SecretId: secretArn,
})
);
if (!result.SecretString) {
throw new Error('SecretString was empty for DB_SECRET_ARN');
}
if (!result.SecretString) {
throw new Error('SecretString was empty for DB_SECRET_ARN');
}
const parsed = JSON.parse(result.SecretString) as Partial<DbSecret>;
if (!parsed.username || !parsed.password) {
throw new Error('DB secret missing username/password');
}
const parsed = JSON.parse(result.SecretString) as Partial<DbSecret>;
if (!parsed.username || !parsed.password) {
throw new Error('DB secret missing username/password');
}
cachedSecret = { username: parsed.username, password: parsed.password };
return cachedSecret;
cachedSecret = { username: parsed.username, password: parsed.password };
return cachedSecret;
}
export async function getPool(): Promise<Pool> {
if (cachedPool) return cachedPool;
if (cachedPool) return cachedPool;
const host = requireEnv('DB_HOST');
const port = Number.parseInt(requireEnv('DB_PORT'), 10);
const database = requireEnv('DB_NAME');
const { username: user, password } = await getDbSecret();
const host = requireEnv('DB_HOST');
const port = Number.parseInt(requireEnv('DB_PORT'), 10);
const database = requireEnv('DB_NAME');
const { username: user, password } = await getDbSecret();
cachedPool = new Pool({
host,
port,
database,
user,
password,
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
});
cachedPool = new Pool({
host,
port,
database,
user,
password,
ssl: {
rejectUnauthorized: true,
},
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
});
return cachedPool;
return cachedPool;
}
export async function getDb(): Promise<NodePgDatabase<typeof schema>> {
if (cachedDb) return cachedDb;
const pool = await getPool();
cachedDb = drizzle(pool, { schema });
return cachedDb;
if (cachedDb) return cachedDb;
const pool = await getPool();
cachedDb = drizzle(pool, { schema });
return cachedDb;
}