SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.6 KB · 37 lines typescript
Raw Blame History
1import { NextResponse } from 'next/server';2import { screenAssetsExport } from '@/lib/queries/screener';3import { SCREENER_EXPORT_MAX, isPlausibleChange, parseScreenerParams, toCsv } from '@/lib/screener';4import type { SP } from '@/lib/search-params';56export const runtime = 'nodejs';7export const dynamic = 'force-dynamic';89const ATTRIBUTION = 'Source: RareIndex (www.rareindex.io). Valuations are model estimates with stated confidence; not investment advice.';1011const 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'];1213/** GET /screener/export?…same filters…[&format=json] — current screen, capped at 5 000 rows (§160). */14export async function GET(req: Request) {15  const url = new URL(req.url);16  const sp: SP = Object.fromEntries(url.searchParams.entries());17  const filters = parseScreenerParams(sp);18  const items = await screenAssetsExport(filters, SCREENER_EXPORT_MAX);19  const asOf = new Date().toISOString();20  const held = (v: number | null) => (isPlausibleChange(v) ? v : null); // artefacts are exported as empty, never as numbers21  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}`]);22  if (url.searchParams.get('format') === 'json') {23    return NextResponse.json(24      { 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 } },25      { headers: { 'cache-control': 'private, max-age=60' } },26    );27  }28  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'}`]);29  return new NextResponse(csv, {30    headers: {31      'content-type': 'text/csv; charset=utf-8',32      'content-disposition': 'attachment; filename="rareindex-screener.csv"',33      'cache-control': 'private, max-age=60',34    },35  });36}37