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.8 KB · 91 lines typescript
Raw Blame History
1import { fileURLToPath } from 'node:url';2import path from 'node:path';3import { eq, sql } from 'drizzle-orm';4import { getDb, closeDb } from './client.js';5import { biomarkers, geographies, metricDefinitions } from './schema/index.js';6import { mintId } from './ids.js';7import { METRIC_CATALOG } from './seed-data/metrics.js';8import { GEOGRAPHY_SEED } from './seed-data/geographies.js';9import { BIOMARKER_SEED, buildMeasurement, seedGeneSymbols } from './seed-data/biomarkers.js';10import type { Database } from './client.js';1112/**13 * Canonical biomarkers (SPEC §17, §52, §121): curated metadata only — identity, verified NCIt code,14 * anchor gene, aliases, assay conventions. Every scientific link is derived at query time.15 * Idempotent by slug; `CI-BIO` ids are minted once and never reassigned. Anchor genes are resolved16 * against `genes` by HGNC symbol; an unresolved symbol is reported and left null, never invented.17 */18export async function seedBiomarkers(db: Database): Promise<{ inserted: number; updated: number; total: number; unresolvedGenes: string[] }> {19  const symbols = seedGeneSymbols();20  const rows = symbols.length ? await db.execute<{ id: string; symbol: string }>(sql`SELECT id, symbol FROM genes WHERE symbol = ANY(${sql.param(symbols)}::text[])`) : [];21  const geneId = new Map<string, string>();22  for (const r of rows) geneId.set(r.symbol, r.id);23  const unresolvedGenes = symbols.filter((s) => !geneId.has(s));24  let inserted = 0;25  let updated = 0;26  for (const b of BIOMARKER_SEED) {27    const values = {28      name: b.name,29      kind: b.kind,30      geneId: b.geneSymbol ? geneId.get(b.geneSymbol) ?? null : null,31      ncitCode: b.ncitCode,32      description: b.description,33      measurement: buildMeasurement(b) as unknown as Record<string, unknown>,34    };35    const [existing] = await db.select({ id: biomarkers.id }).from(biomarkers).where(eq(biomarkers.slug, b.slug)).limit(1);36    if (existing) {37      await db.update(biomarkers).set({ ...values, updatedAt: new Date() }).where(eq(biomarkers.id, existing.id));38      updated++;39    } else {40      await db.insert(biomarkers).values({ id: await mintId(db, 'BIO'), slug: b.slug, ...values });41      inserted++;42    }43  }44  const [{ n }] = (await db.execute<{ n: string }>(sql`SELECT count(*)::text AS n FROM biomarkers`)) as unknown as [{ n: string }];45  return { inserted, updated, total: Number(n), unresolvedGenes };46}4748/**49 * Seeds only system data (CLAUDE.md §358): metric definitions, canonical geographies and the50 * curated biomarker catalogue (metadata, not scientific values). Scientific data is never seeded —51 * it comes from connectors. Sources are synced from connector manifests by `pnpm cix sources:sync`52 * (lives in the root CLI to avoid a package cycle).53 */54export async function seed(): Promise<void> {55  const db = getDb({ max: 2 });56  for (const m of METRIC_CATALOG) {57    const [existing] = await db.select({ id: metricDefinitions.id }).from(metricDefinitions).where(eq(metricDefinitions.slug, m.slug)).limit(1);58    const values = { ...m, updatedAt: new Date() };59    if (existing) await db.update(metricDefinitions).set(values).where(eq(metricDefinitions.id, existing.id));60    else await db.insert(metricDefinitions).values({ id: await mintId(db, 'METRIC'), ...m });61  }62  const bySlug = new Map<string, string>();63  for (const g of GEOGRAPHY_SEED) {64    const [existing] = await db.select({ id: geographies.id }).from(geographies).where(eq(geographies.slug, g.slug)).limit(1);65    const parentId = g.parentSlug ? bySlug.get(g.parentSlug) ?? null : null;66    if (existing) {67      await db.update(geographies).set({ name: g.name, kind: g.kind, iso2: g.iso2 ?? null, iso3: g.iso3 ?? null, parentId, whoRegion: g.whoRegion ?? null }).where(eq(geographies.id, existing.id));68      bySlug.set(g.slug, existing.id);69    } else {70      const id = await mintId(db, 'GEO');71      await db.insert(geographies).values({ id, slug: g.slug, name: g.name, kind: g.kind, iso2: g.iso2 ?? null, iso3: g.iso3 ?? null, parentId, whoRegion: g.whoRegion ?? null });72      bySlug.set(g.slug, id);73    }74  }75  const [{ n }] = (await db.execute<{ n: string }>(sql`SELECT count(*)::text AS n FROM metric_definitions`)) as unknown as [{ n: string }];76  const bio = await seedBiomarkers(db);77  console.log(`[seed] metrics=${n} geographies=${GEOGRAPHY_SEED.length} biomarkers=${bio.total} (inserted=${bio.inserted} updated=${bio.updated})`);78  if (bio.unresolvedGenes.length) console.warn(`[seed] biomarkers: unresolved HGNC symbols (gene_id left null): ${bio.unresolvedGenes.join(', ')}`);79}8081const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);82if (isMain) {83  seed()84    .then(() => closeDb())85    .catch(async (e) => {86      console.error('[seed] failed', e);87      await closeDb();88      process.exit(1);89    });90}91