spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import { z } from 'zod';23/**4 * Response envelope for every /v1 endpoint (CLAUDE.md §321):5 * { data, sources: [...], dataRelease: "CancerIndex <YYYY-MM>", generatedAt, ...pagination }6 * `sources` lists the distinct upstream sources behind the returned data with their license and7 * attribution so consumers can comply with redistribution terms (§142).8 */9export const SourceRef = z.object({10 id: z.string().describe('CI-SOURCE-… identifier'),11 slug: z.string().describe('Connector id (e.g. "oncotree")'),12 name: z.string(),13 license: z.string().nullable(),14 attribution: z.string().nullable(),15 url: z.string().nullable().describe('Source homepage'),16});17export type SourceRef = z.infer<typeof SourceRef>;1819export const Pagination = z.object({20 total: z.number().int().nonnegative().describe('Total rows matching the query (before limit/offset)'),21 limit: z.number().int().positive(),22 offset: z.number().int().nonnegative(),23 hasMore: z.boolean(),24});25export type Pagination = z.infer<typeof Pagination>;2627export interface Envelope<T> {28 data: T;29 sources: SourceRef[];30 dataRelease: string;31 generatedAt: string;32 total?: number;33 limit?: number;34 offset?: number;35 hasMore?: boolean;36}3738/** Zod schema factory for OpenAPI generation: envelope around a data schema. */39export function envelopeSchema<T extends z.ZodTypeAny>(data: T, paginated = false) {40 const base = z.object({41 data,42 sources: z.array(SourceRef),43 dataRelease: z.string().describe('Data release label, "CancerIndex YYYY-MM"'),44 generatedAt: z.string().describe('ISO-8601 timestamp of the response'),45 });46 return paginated ? base.extend(Pagination.shape) : base;47}4849/** "CancerIndex 2026-09" — month of the most recent successful ingest run, or of the request when unknown. */50export function dataRelease(asOf: Date = new Date()): string {51 return `CancerIndex ${asOf.toISOString().slice(0, 7)}`;52}5354export function envelope<T>(data: T, sources: SourceRef[], pagination?: Pagination, asOf?: Date): Envelope<T> {55 const out: Envelope<T> = { data, sources: dedupeSources(sources), dataRelease: dataRelease(asOf), generatedAt: new Date().toISOString() };56 if (pagination) Object.assign(out, pagination);57 return out;58}5960export function paginate(total: number, limit: number, offset: number): Pagination {61 return { total, limit, offset, hasMore: offset + limit < total };62}6364/** Distinct by id, stable order by slug. */65export function dedupeSources(list: SourceRef[]): SourceRef[] {66 const seen = new Map<string, SourceRef>();67 for (const s of list) if (!seen.has(s.id)) seen.set(s.id, s);68 return [...seen.values()].sort((a, b) => a.slug.localeCompare(b.slug));69}70