SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
2.6 KB · 70 lines typescript
Raw Blame History
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