import { NextResponse } from 'next/server'; import { screenAssetsExport } from '@/lib/queries/screener'; import { SCREENER_EXPORT_MAX, isPlausibleChange, parseScreenerParams, toCsv } from '@/lib/screener'; import type { SP } from '@/lib/search-params'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; const ATTRIBUTION = 'Source: RareIndex (www.rareindex.io). Valuations are model estimates with stated confidence; not investment advice.'; const HEADER = ['asset_id', 'slug', 'title', 'category', 'year', 'brand', 'set', 'riv_usd', 'riv_low_usd', 'riv_high_usd', 'riv_confidence', 'riv_sample_size', 'change_30d', 'change_1y', 'liquidity_score', 'rarity_score', 'sales_30d', 'sales_1y', 'sales_total', 'active_listings', 'min_ask_usd', 'spread_vs_riv', 'value_opportunity', 'ath_usd', 'drawdown', 'volume_30d_usd', 'stats_updated_at', 'url']; /** GET /screener/export?…same filters…[&format=json] — current screen, capped at 5 000 rows (§160). */ export async function GET(req: Request) { const url = new URL(req.url); const sp: SP = Object.fromEntries(url.searchParams.entries()); const filters = parseScreenerParams(sp); const items = await screenAssetsExport(filters, SCREENER_EXPORT_MAX); const asOf = new Date().toISOString(); const held = (v: number | null) => (isPlausibleChange(v) ? v : null); // artefacts are exported as empty, never as numbers const rows = items.map((a) => [a.id, a.slug, a.title, a.categorySlug, a.year, a.brand, a.setName, a.rivUsd, a.rivLowUsd, a.rivHighUsd, a.rivConfidence, a.rivSampleSize, held(a.change30d), held(a.change1y), a.liquidityScore, a.rarityScore, a.sales30d, a.sales1y, a.salesCount, a.activeListings, a.minAskUsd, a.spread, a.valueOpportunity, a.athUsd, a.drawdown, a.volume30dUsd, a.updatedAt, `https://www.rareindex.io/asset/${a.slug}`]); if (url.searchParams.get('format') === 'json') { return NextResponse.json( { data: rows.map((r) => Object.fromEntries(HEADER.map((h, i) => [h, r[i] ?? null]))), meta: { count: rows.length, as_of: asOf, truncated: items.length >= SCREENER_EXPORT_MAX, attribution: ATTRIBUTION, filters } }, { headers: { 'cache-control': 'private, max-age=60' } }, ); } const csv = toCsv(HEADER, rows, [`as_of=${asOf}`, ATTRIBUTION, `rows=${rows.length}${items.length >= SCREENER_EXPORT_MAX ? ' (truncated to the export cap)' : ''}`, `filters=${url.search.replace(/^\?/, '') || 'none'}`]); return new NextResponse(csv, { headers: { 'content-type': 'text/csv; charset=utf-8', 'content-disposition': 'attachment; filename="rareindex-screener.csv"', 'cache-control': 'private, max-age=60', }, }); }