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%
2.8 KB · 43 lines typescript
Raw Blame History
1import { getMetric, getSnapshot, rankingRows } from '@/lib/queries/rankings';2import { sourceInfoById } from '@/lib/queries/provenance';3import { SITE_URL } from '@/lib/site';4import { isoDate, toDate } from '@/lib/format';56export const dynamic = 'force-dynamic';78function csvCell(v: unknown): string {9  const s = v == null ? '' : String(v);10  return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;11}1213/**14 * GET /api/export/rankings.csv?metric=<slug>&scope=<scope_key>15 * CancerIndex-derived ranking snapshot as CSV. Attribution header rows precede the data (§ /data).16 */17export async function GET(req: Request) {18  const url = new URL(req.url);19  const metricSlug = url.searchParams.get('metric') ?? '';20  const scope = url.searchParams.get('scope');21  if (!/^[a-z0-9_]{2,64}$/.test(metricSlug)) return new Response('invalid metric', { status: 400 });22  const metric = await getMetric(metricSlug);23  if (!metric) return new Response('unknown metric', { status: 404 });24  const snap = await getSnapshot(metricSlug, scope);25  if (!snap) return new Response('no snapshot for this metric/scope', { status: 404 });26  const rows = await rankingRows(snap.id, 100_000);27  const src = await sourceInfoById(snap.source_ids);28  const sources = snap.source_ids.map((id) => src.get(id)?.name ?? id).join('; ');2930  const header = [31    `# CancerIndex ranking export — ${metric.name}`,32    `# Source: CancerIndex (${SITE_URL}) — derived data, CC BY 4.0. Underlying observations remain under their providers' licenses: ${sources || 'see methodology'}.`,33    `# Metric: ${metric.slug} · formula: ${metric.formula} · formula_version: ${snap.formula_version}`,34    `# Scope: ${snap.scope_key} · eligible_entities: ${snap.eligible_entities} · generated_at: ${toDate(snap.generated_at)?.toISOString() ?? String(snap.generated_at)} · inputs_hash: ${snap.inputs_hash}`,35    `# Unit: ${metric.unit} · ${metric.higher_is_worse == null ? 'neutral direction' : metric.higher_is_worse ? 'higher is worse' : 'higher is better'} · Ties share a rank. Population statistics do not predict individual outcomes.`,36  ];37  const cols = ['rank', 'previous_rank', 'cancer_id', 'slug', 'canonical_name', 'value', 'unit', 'percentile', 'confidence', 'eligible_entities', 'inputs_json'];38  const lines = rows.map((r) => [r.rank, r.previous_rank ?? '', r.cancer_id, r.slug, r.canonical_name, r.value, r.unit, r.percentile, r.confidence, r.eligible_entities, JSON.stringify(r.inputs)].map(csvCell).join(','));39  const body = [...header, cols.join(','), ...lines].join('\n') + '\n';40  const fname = `cancerindex-${metric.slug}-${snap.scope_key.replace(/[^a-z0-9]+/gi, '_')}-${isoDate(snap.generated_at)}.csv`;41  return new Response(body, { headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': `attachment; filename="${fname}"`, 'Cache-Control': 'public, max-age=300' } });42}43