import { sql } from 'drizzle-orm'; import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'; import { z } from 'zod'; import { resolveVariant } from '../lib/resolve.js'; import { AnyRecord, camel, camelRows, ok, respond } from '../lib/respond.js'; import { pluck } from '../lib/sources.js'; export const variantRoutes: FastifyPluginAsyncZod = async (app) => { app.get('/variants/:id', { schema: { tags: ['variants'], summary: 'Variant: coordinates per assembly, ClinVar interpretations (structured), CIViC evidence grouped by cancer (§50), knowledge edges', params: z.object({ id: z.string().min(1).describe('CI-VAR-… id or slug') }), response: ok(AnyRecord) } }, async (req) => { const { id } = await resolveVariant(app.db, req.params.id); const db = app.db; const [variant, aliases, clinsig, evidence, edges, pubs] = await Promise.all([ db.execute>(sql`SELECT v.*, g.name AS gene_name, g.hgnc_id FROM variants v LEFT JOIN genes g ON g.id = v.gene_id WHERE v.id = ${id}`), db.execute>(sql`SELECT alias, source_id FROM variant_aliases WHERE variant_id = ${id} ORDER BY alias`), db.execute>(sql`SELECT cs.*, p.source_id, p.source_url, p.retrieved_at, p.dataset_version FROM variant_clinical_significance cs LEFT JOIN provenance p ON p.id = cs.provenance_id WHERE cs.variant_id = ${id}`), db.execute>(sql` SELECT e.civic_id, e.name, e.molecular_profile_name, e.disease_name, e.doid, e.cancer_id, c.slug AS cancer_slug, c.canonical_name AS cancer_name, e.cancer_match_type, e.therapy_names, e.therapy_ids, e.therapy_interaction_type, e.evidence_type, e.evidence_level, e.evidence_direction, e.significance, e.evidence_rating, e.status, e.description, e.pmid, e.source_citation, e.phenotypes, p.source_id, p.source_url, p.retrieved_at FROM civic_evidence_items e LEFT JOIN cancers c ON c.id = e.cancer_id LEFT JOIN provenance p ON p.id = e.provenance_id WHERE ${id} = ANY(e.variant_ids) ORDER BY e.cancer_id NULLS LAST, e.evidence_level, e.civic_id`), db.execute>(sql` SELECT k.id, k.source_entity_type, k.source_entity_id, k.target_entity_type, k.target_entity_id, k.relationship_type, k.cancer_context_ids, k.direction, k.evidence_level, k.evidence_category, k.status, k.source_id, k.support_count, k.last_seen_at FROM knowledge_edges k WHERE (k.source_entity_type = 'variant' AND k.source_entity_id = ${id}) OR (k.target_entity_type = 'variant' AND k.target_entity_id = ${id}) ORDER BY k.relationship_type LIMIT 500`), db.execute>(sql` SELECT p.id, p.pmid, p.doi, p.title, p.journal, p.pub_year, p.retracted, e.method, e.status AS edge_status, e.source_id FROM publication_entity_edges e JOIN publications p ON p.id = e.publication_id WHERE e.entity_type = 'variant' AND e.entity_id = ${id} AND e.status <> 'rejected' ORDER BY p.pub_year DESC NULLS LAST LIMIT 100`), ]); // Evidence separated by cancer context — never pooled across diseases (CLAUDE.md §50). const byCancer = new Map | null; items: Array>; accepted: number }>(); for (const e of evidence) { const key = (e.cancer_id as string | null) ?? `unmapped:${e.disease_name ?? 'unknown'}`; if (!byCancer.has(key)) byCancer.set(key, { cancer: e.cancer_id ? { id: e.cancer_id, slug: e.cancer_slug, name: e.cancer_name, matchType: e.cancer_match_type } : null, items: [], accepted: 0 }); const grp = byCancer.get(key)!; if (e.status === 'ACCEPTED') grp.accepted++; const { cancer_slug: _a, cancer_name: _b, cancer_match_type: _c, source_id, source_url, retrieved_at, ...rest } = e; grp.items.push({ ...camel(rest), provenance: { sourceId: source_id, url: source_url, retrievedAt: retrieved_at, category: 'curated_evidence' } }); } const data = { ...camel(variant[0]!), aliases: camelRows(aliases), clinicalSignificance: clinsig.map((r) => { const { source_id, source_url, retrieved_at, dataset_version, provenance_id: _p, ...rest } = r; return { ...camel(rest), provenance: { sourceId: source_id, url: source_url, retrievedAt: retrieved_at, datasetVersion: dataset_version, category: 'curated_evidence' } }; }), evidenceByCancer: [...byCancer.values()].map((g) => ({ cancer: g.cancer, acceptedItems: g.accepted, items: g.items })), edges: camelRows(edges), publications: pubs.map((r) => { const { method, edge_status, source_id, ...rest } = r; return { ...camel(rest), edge: { method, status: edge_status, sourceId: source_id } }; }), }; return respond(app, data, [...pluck(clinsig, 'source_id'), ...pluck(evidence, 'source_id'), ...pluck(edges, 'source_id'), ...pluck(pubs, 'source_id'), ...pluck(aliases, 'source_id')]); }); };