import { createHash } from "node:crypto"; import { and, desc, eq, max } from "drizzle-orm"; import { drizzle } from "drizzle-orm/bun-sqlite"; import { getSqliteClient } from "@/lib/server/db"; import { issuerOverlay, issuerOverlayRevision, schema, type IssuerOverlayDefinition, type IssuerOverlayDiagnostics, type IssuerOverlayStats, } from "@/lib/server/db/schema"; type IssuerOverlayRevisionRecord = { id: number; ticker: string; revision_number: number; definition_hash: string; definition_json: IssuerOverlayDefinition; diagnostics_json: IssuerOverlayDiagnostics | null; source_snapshot_ids: number[]; created_at: string; }; type IssuerOverlayRecord = { ticker: string; status: "empty" | "active" | "error"; active_revision_id: number | null; last_built_at: string | null; last_error: string | null; stats_json: IssuerOverlayStats | null; created_at: string; updated_at: string; active_revision: IssuerOverlayRevisionRecord | null; }; function getDb() { return drizzle(getSqliteClient(), { schema }); } function normalizeTicker(ticker: string) { return ticker.trim().toUpperCase(); } function uniqueSorted(values: string[]) { return [...new Set(values.filter((value) => value.trim().length > 0))].sort( (left, right) => left.localeCompare(right), ); } export function normalizeIssuerOverlayDefinition( input: IssuerOverlayDefinition, ): IssuerOverlayDefinition { return { version: "fiscal-v1", ticker: normalizeTicker(input.ticker), pack: input.pack?.trim() ? input.pack.trim() : null, mappings: [...input.mappings] .map((mapping) => ({ surface_key: mapping.surface_key, statement: mapping.statement, allowed_source_concepts: uniqueSorted(mapping.allowed_source_concepts), allowed_authoritative_concepts: uniqueSorted( mapping.allowed_authoritative_concepts, ), })) .filter( (mapping) => mapping.allowed_source_concepts.length > 0 || mapping.allowed_authoritative_concepts.length > 0, ) .sort((left, right) => { return ( left.statement.localeCompare(right.statement) || left.surface_key.localeCompare(right.surface_key) ); }), }; } function definitionHash(definition: IssuerOverlayDefinition) { return createHash("sha256") .update(JSON.stringify(normalizeIssuerOverlayDefinition(definition))) .digest("hex"); } function toRevisionRecord( row: typeof issuerOverlayRevision.$inferSelect, ): IssuerOverlayRevisionRecord { const definition = row.definition_json; if (!definition) { throw new Error( `Issuer overlay revision ${row.id} is missing definition_json`, ); } return { id: row.id, ticker: row.ticker, revision_number: row.revision_number, definition_hash: row.definition_hash, definition_json: normalizeIssuerOverlayDefinition(definition), diagnostics_json: row.diagnostics_json ?? null, source_snapshot_ids: row.source_snapshot_ids ?? [], created_at: row.created_at, }; } async function getRevisionById(id: number | null) { if (!id) { return null; } const [row] = await getDb() .select() .from(issuerOverlayRevision) .where(eq(issuerOverlayRevision.id, id)) .limit(1); return row ? toRevisionRecord(row) : null; } export async function getIssuerOverlay(ticker: string) { const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } const [row] = await getDb() .select() .from(issuerOverlay) .where(eq(issuerOverlay.ticker, normalizedTicker)) .limit(1); if (!row) { return null; } return { ticker: row.ticker, status: row.status, active_revision_id: row.active_revision_id ?? null, last_built_at: row.last_built_at, last_error: row.last_error, stats_json: row.stats_json ?? null, created_at: row.created_at, updated_at: row.updated_at, active_revision: await getRevisionById(row.active_revision_id ?? null), } satisfies IssuerOverlayRecord; } export async function ensureIssuerOverlayRow(ticker: string) { const normalizedTicker = normalizeTicker(ticker); if (!normalizedTicker) { return null; } const now = new Date().toISOString(); await getDb() .insert(issuerOverlay) .values({ ticker: normalizedTicker, status: "empty", active_revision_id: null, last_built_at: null, last_error: null, stats_json: null, created_at: now, updated_at: now, }) .onConflictDoNothing(); return await getIssuerOverlay(normalizedTicker); } export async function markIssuerOverlayBuildState(input: { ticker: string; status: "empty" | "active" | "error"; lastError?: string | null; stats?: IssuerOverlayStats | null; activeRevisionId?: number | null; }) { const normalizedTicker = normalizeTicker(input.ticker); const now = new Date().toISOString(); await ensureIssuerOverlayRow(normalizedTicker); await getDb() .update(issuerOverlay) .set({ status: input.status, last_error: input.lastError ?? null, last_built_at: now, stats_json: input.stats ?? null, active_revision_id: input.activeRevisionId === undefined ? undefined : input.activeRevisionId, updated_at: now, }) .where(eq(issuerOverlay.ticker, normalizedTicker)); return await getIssuerOverlay(normalizedTicker); } export async function getActiveIssuerOverlayDefinition(ticker: string) { const overlay = await getIssuerOverlay(ticker); return overlay?.active_revision?.definition_json ?? null; } export async function publishIssuerOverlayRevision(input: { ticker: string; definition: IssuerOverlayDefinition; diagnostics: IssuerOverlayDiagnostics; stats: IssuerOverlayStats; }) { const normalizedTicker = normalizeTicker(input.ticker); const normalizedDefinition = normalizeIssuerOverlayDefinition({ ...input.definition, ticker: normalizedTicker, }); const hash = definitionHash(normalizedDefinition); const now = new Date().toISOString(); return await getDb().transaction(async (tx) => { const [currentOverlay] = await tx .select() .from(issuerOverlay) .where(eq(issuerOverlay.ticker, normalizedTicker)) .limit(1); if (!currentOverlay) { await tx.insert(issuerOverlay).values({ ticker: normalizedTicker, status: "empty", active_revision_id: null, last_built_at: now, last_error: null, stats_json: null, created_at: now, updated_at: now, }); } const [existingRevision] = await tx .select() .from(issuerOverlayRevision) .where( and( eq(issuerOverlayRevision.ticker, normalizedTicker), eq(issuerOverlayRevision.definition_hash, hash), ), ) .limit(1); const currentActiveRevisionId = currentOverlay?.active_revision_id ?? null; if (existingRevision) { await tx .update(issuerOverlay) .set({ status: normalizedDefinition.mappings.length > 0 ? "active" : "empty", active_revision_id: normalizedDefinition.mappings.length > 0 ? existingRevision.id : currentActiveRevisionId, last_built_at: now, last_error: null, stats_json: input.stats, updated_at: now, }) .where(eq(issuerOverlay.ticker, normalizedTicker)); return { published: normalizedDefinition.mappings.length > 0 && currentActiveRevisionId !== existingRevision.id, revision: toRevisionRecord(existingRevision), }; } const [currentRevisionNumberRow] = await tx .select({ value: max(issuerOverlayRevision.revision_number), }) .from(issuerOverlayRevision) .where(eq(issuerOverlayRevision.ticker, normalizedTicker)); const nextRevisionNumber = (currentRevisionNumberRow?.value ?? 0) + 1; const [savedRevision] = await tx .insert(issuerOverlayRevision) .values({ ticker: normalizedTicker, revision_number: nextRevisionNumber, definition_hash: hash, definition_json: normalizedDefinition, diagnostics_json: input.diagnostics, source_snapshot_ids: input.diagnostics.sampledSnapshotIds, created_at: now, }) .returning(); await tx .update(issuerOverlay) .set({ status: normalizedDefinition.mappings.length > 0 ? "active" : "empty", active_revision_id: normalizedDefinition.mappings.length > 0 ? savedRevision.id : currentActiveRevisionId, last_built_at: now, last_error: null, stats_json: input.stats, updated_at: now, }) .where(eq(issuerOverlay.ticker, normalizedTicker)); return { published: normalizedDefinition.mappings.length > 0, revision: toRevisionRecord(savedRevision), }; }); } export async function listIssuerOverlayRevisions(ticker: string) { const normalizedTicker = normalizeTicker(ticker); const rows = await getDb() .select() .from(issuerOverlayRevision) .where(eq(issuerOverlayRevision.ticker, normalizedTicker)) .orderBy(desc(issuerOverlayRevision.revision_number)); return rows.map(toRevisionRecord); }