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 · 50 lines typescript
Raw Blame History
1import { listGapScopes, pickGapScope, gapComponents, gapSums, gapScopeKey } from '@/lib/queries/research-gap';2import { SITE_URL } from '@/lib/site';3import { isoDate, toDate } from '@/lib/format';45export const dynamic = 'force-dynamic';67function csvCell(v: unknown): string {8  const s = v == null ? '' : String(v);9  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;10}1112/**13 * GET /api/export/research-gap.csv?geography=USA&year=2024&sex=all&source=cdc-wonder14 * Research-gap components for one burden scope as CSV. Attribution header rows precede the data15 * (same convention as /api/export/rankings.csv): CancerIndex-derived values, with the burden source16 * and the activity sources named so the file can be cited correctly.17 */18export async function GET(req: Request) {19  const url = new URL(req.url);20  const geography = (url.searchParams.get('geography') ?? 'USA').toUpperCase();21  const yearRaw = url.searchParams.get('year');22  const year = yearRaw && /^\d{4}$/.test(yearRaw) ? Number(yearRaw) : null;23  const sexRaw = url.searchParams.get('sex') ?? 'all';24  const sex = ['all', 'male', 'female'].includes(sexRaw) ? sexRaw : 'all';25  const source = url.searchParams.get('source') ?? '';26  if (!/^[A-Z0-9_-]{2,16}$/.test(geography) || !/^[a-z0-9-]{0,64}$/.test(source)) return new Response('invalid scope', { status: 400 });27  const scopes = await listGapScopes();28  const scope = pickGapScope(scopes, { geography, year, sex, source });29  if (!scope) return new Response('no research-gap components for this scope', { status: 404 });30  const rows = await gapComponents(scope);31  const sums = gapSums(rows);3233  const header = [34    `# CancerIndex Research Gap Index export — ${scope.geography} ${scope.year} · ${scope.sex === 'all' ? 'both sexes' : scope.sex} · all ages · top-level cancers`,35    `# Source: CancerIndex (${SITE_URL}) — derived data, CC BY 4.0. Underlying observations remain under their providers' licenses: deaths from ${scope.source_name} (${scope.source_slug}); active interventional trials from ClinicalTrials.gov (cancer + NCIt descendants); publications (last 5 years) from PubMed (query-based per entity).`,36    `# Formulas: death_share = deaths / Σ deaths; trial_share = active_trials / Σ active_trials; publication_share = publications_5y / Σ publications_5y (sums over eligible cancers, deaths ≥ 100); trial_gap_ratio = log2(death_share / trial_share); research_gap_ratio = log2(death_share / publication_share); *_per_1000_deaths = activity / (deaths / 1000).`,37    `# formula_version: ${scope.formula_version} · scope_key: ${gapScopeKey(scope)} · eligible_entities: ${sums.eligible} of ${rows.length} · sum_deaths: ${sums.deaths} · sum_active_trials: ${sums.activeTrials} · sum_publications_5y: ${sums.publications5y} · computed_at: ${toDate(scope.computed_at)?.toISOString() ?? String(scope.computed_at)}`,38    `# A gap ratio is a quantitative signal about registered activity relative to deaths — not a judgement of research quality or funding. Ratios are relative to the eligible set of this scope only; compare within one file. United States burden only while global estimates are under license review.`,39  ];40  const cols = ['cancer_id', 'slug', 'canonical_name', 'geography', 'year', 'sex', 'burden_source', 'deaths', 'incidence', 'active_trials', 'phase3_trials', 'publications_5y', 'approved_drugs', 'death_share', 'trial_share', 'publication_share', 'trial_gap_ratio', 'research_gap_ratio', 'trials_per_1000_deaths', 'publications_per_1000_deaths', 'trial_gap_rank', 'research_gap_rank', 'eligible', 'ineligible_reason', 'formula_version', 'inputs_json'];41  const lines = rows.map((r) =>42    [r.cancer_id, r.slug, r.canonical_name, scope.geography, scope.year, scope.sex, scope.source_slug, r.deaths, r.incidence ?? '', r.active_trials, r.phase3_trials, r.publications_5y, r.approved_drugs, r.death_share ?? '', r.trial_share ?? '', r.publication_share ?? '', r.trial_gap_ratio ?? '', r.research_gap_ratio ?? '', r.trials_per_1000_deaths ?? '', r.publications_per_1000_deaths ?? '', r.trial_gap_rank ?? '', r.research_gap_rank ?? '', r.eligible, r.ineligible_reason ?? '', r.formula_version, JSON.stringify(r.inputs)]43      .map(csvCell)44      .join(','),45  );46  const body = [...header, cols.join(','), ...lines].join('\n') + '\n';47  const fname = `cancerindex-research-gap-${scope.geography}-${scope.year}-${scope.sex}-${scope.source_slug}-${isoDate(scope.computed_at)}.csv`;48  return new Response(body, { headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': `attachment; filename="${fname}"`, 'Cache-Control': 'public, max-age=300' } });49}50