spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { FastifyInstance } from 'fastify';2import { z } from 'zod';3import { TtlCache } from './cache.js';4import { envelope, envelopeSchema, type Envelope, type Pagination } from './envelope.js';5import { latestDataAsOf, loadSources } from './sources.js';67const asOfCache = new TtlCache<Date | 'none'>(60_000);89/** Build the envelope: resolves source references and the data-release month (cached 60 s). */10export async function respond<T>(app: FastifyInstance, data: T, sourceRefs: Iterable<string | null | undefined>, pagination?: Pagination): Promise<Envelope<T>> {11 const [sources, asOf] = await Promise.all([loadSources(app.db, sourceRefs), asOfCache.getOrLoad('asOf', async () => (await latestDataAsOf(app.db)) ?? 'none')]);12 return envelope(data, sources, pagination, asOf === 'none' ? undefined : asOf);13}1415/** Loose schemas for OpenAPI: payload shapes are documented in docs/API.md and DATA-MODEL.md. */16export const AnyRecord = z.record(z.string(), z.unknown());17export const AnyList = z.array(AnyRecord);18export const ok = (data: z.ZodTypeAny, paginated = false) => ({ 200: envelopeSchema(data, paginated) });1920export const ErrorResponse = z.object({ error: z.object({ code: z.string(), message: z.string(), details: z.unknown().optional() }), requestId: z.string() });2122/** snake_case → camelCase for raw SQL rows (shallow; jsonb values are left untouched). */23export function camel<T = Record<string, unknown>>(row: Record<string, unknown>): T {24 const out: Record<string, unknown> = {};25 for (const [k, v] of Object.entries(row)) out[k.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase())] = v;26 return out as T;27}28export function camelRows<T = Record<string, unknown>>(rows: Iterable<Record<string, unknown>>): T[] {29 return [...rows].map((r) => camel<T>(r));30}3132export function num(v: unknown): number {33 return v === null || v === undefined ? 0 : Number(v);34}35