import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { TtlCache } from './cache.js'; import { envelope, envelopeSchema, type Envelope, type Pagination } from './envelope.js'; import { latestDataAsOf, loadSources } from './sources.js'; const asOfCache = new TtlCache(60_000); /** Build the envelope: resolves source references and the data-release month (cached 60 s). */ export async function respond(app: FastifyInstance, data: T, sourceRefs: Iterable, pagination?: Pagination): Promise> { const [sources, asOf] = await Promise.all([loadSources(app.db, sourceRefs), asOfCache.getOrLoad('asOf', async () => (await latestDataAsOf(app.db)) ?? 'none')]); return envelope(data, sources, pagination, asOf === 'none' ? undefined : asOf); } /** Loose schemas for OpenAPI: payload shapes are documented in docs/API.md and DATA-MODEL.md. */ export const AnyRecord = z.record(z.string(), z.unknown()); export const AnyList = z.array(AnyRecord); export const ok = (data: z.ZodTypeAny, paginated = false) => ({ 200: envelopeSchema(data, paginated) }); export const ErrorResponse = z.object({ error: z.object({ code: z.string(), message: z.string(), details: z.unknown().optional() }), requestId: z.string() }); /** snake_case → camelCase for raw SQL rows (shallow; jsonb values are left untouched). */ export function camel>(row: Record): T { const out: Record = {}; for (const [k, v] of Object.entries(row)) out[k.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase())] = v; return out as T; } export function camelRows>(rows: Iterable>): T[] { return [...rows].map((r) => camel(r)); } export function num(v: unknown): number { return v === null || v === undefined ? 0 : Number(v); }