spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
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