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%
4.5 KB · 97 lines typescript
Raw Blame History
1import { sql } from 'drizzle-orm';2import type { Database } from '@cancerindex/database';3import { isCiId, type IdNamespace } from '@cancerindex/shared';4import { NotFound } from './errors.js';56export type RefKind = 'id' | 'slug' | 'symbol' | 'nct' | 'pmid';78/**9 * Classify a public reference (pure, unit-tested): CI-<NS>-00000001 → id; NCT… → nct; digits → pmid;10 * otherwise a slug/symbol. Never accepts database integers as entity references (CLAUDE.md §6).11 */12export function classifyRef(ref: string, ns: IdNamespace): RefKind {13  const r = ref.trim();14  if (isCiId(r, ns)) return 'id';15  if (ns === 'TRIAL' && /^NCT\d{8}$/i.test(r)) return 'nct';16  if (ns === 'PUB' && /^\d{1,9}$/.test(r)) return 'pmid';17  if (ns === 'GENE') return 'symbol';18  return 'slug';19}2021async function one<T extends Record<string, unknown>>(db: Database, query: ReturnType<typeof sql>): Promise<T | null> {22  const rows = await db.execute<T>(query);23  return (rows[0] as T | undefined) ?? null;24}2526/** Cancer by CI-CAN id or slug. Follows `merged_into` so old ids keep resolving (§70). */27export async function resolveCancer(db: Database, ref: string): Promise<{ id: string; slug: string; status: string }> {28  const kind = classifyRef(ref, 'CAN');29  const row =30    kind === 'id'31      ? await one<{ id: string; slug: string; status: string; merged_into: string | null }>(db, sql`SELECT id, slug, status, merged_into FROM cancers WHERE id = ${ref}`)32      : await one<{ id: string; slug: string; status: string; merged_into: string | null }>(db, sql`SELECT id, slug, status, merged_into FROM cancers WHERE slug = ${ref.toLowerCase()}`);33  if (!row) throw new NotFound('cancer', ref);34  if (row.status === 'merged' && row.merged_into) return resolveCancer(db, row.merged_into);35  return { id: row.id, slug: row.slug, status: row.status };36}3738/** Gene by CI-GENE id, HGNC symbol (case-insensitive), previous/alias symbol or HGNC:nnnn. */39export async function resolveGene(db: Database, ref: string): Promise<{ id: string; symbol: string }> {40  const kind = classifyRef(ref, 'GENE');41  if (kind === 'id') {42    const row = await one<{ id: string; symbol: string }>(db, sql`SELECT id, symbol FROM genes WHERE id = ${ref}`);43    if (row) return row;44    throw new NotFound('gene', ref);45  }46  const sym = ref.trim();47  const row = await one<{ id: string; symbol: string }>(48    db,49    sql`SELECT g.id, g.symbol FROM genes g WHERE upper(g.symbol) = upper(${sym}) OR g.hgnc_id = ${sym}50        UNION ALL51        SELECT g.id, g.symbol FROM genes g JOIN gene_aliases a ON a.gene_id = g.id WHERE upper(a.alias) = upper(${sym})52        LIMIT 1`,53  );54  if (!row) throw new NotFound('gene', ref);55  return row;56}5758export async function resolveVariant(db: Database, ref: string): Promise<{ id: string; slug: string }> {59  const kind = classifyRef(ref, 'VAR');60  const row =61    kind === 'id'62      ? await one<{ id: string; slug: string }>(db, sql`SELECT id, slug FROM variants WHERE id = ${ref}`)63      : await one<{ id: string; slug: string }>(db, sql`SELECT id, slug FROM variants WHERE slug = ${ref.toLowerCase()}`);64  if (!row) throw new NotFound('variant', ref);65  return row;66}6768export async function resolveDrug(db: Database, ref: string): Promise<{ id: string; slug: string }> {69  const kind = classifyRef(ref, 'DRUG');70  const row =71    kind === 'id'72      ? await one<{ id: string; slug: string }>(db, sql`SELECT id, slug FROM drugs WHERE id = ${ref}`)73      : await one<{ id: string; slug: string }>(db, sql`SELECT id, slug FROM drugs WHERE slug = ${ref.toLowerCase()}`);74  if (!row) throw new NotFound('drug', ref);75  return row;76}7778export async function resolveTrial(db: Database, ref: string): Promise<{ id: string; nctId: string }> {79  const kind = classifyRef(ref, 'TRIAL');80  const row =81    kind === 'id'82      ? await one<{ id: string; nct_id: string }>(db, sql`SELECT id, nct_id FROM clinical_trials WHERE id = ${ref}`)83      : await one<{ id: string; nct_id: string }>(db, sql`SELECT id, nct_id FROM clinical_trials WHERE nct_id = ${ref.toUpperCase()}`);84  if (!row) throw new NotFound('trial', ref);85  return { id: row.id, nctId: row.nct_id };86}8788export async function resolvePublication(db: Database, ref: string): Promise<{ id: string; pmid: string | null }> {89  const kind = classifyRef(ref, 'PUB');90  const row =91    kind === 'id'92      ? await one<{ id: string; pmid: string | null }>(db, sql`SELECT id, pmid FROM publications WHERE id = ${ref}`)93      : await one<{ id: string; pmid: string | null }>(db, sql`SELECT id, pmid FROM publications WHERE pmid = ${ref}`);94  if (!row) throw new NotFound('publication', ref);95  return row;96}97