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