import type { FastifyReply } from 'fastify'; export const ATTRIBUTION = 'Data © RareIndex.io and its sources; valuations are estimates, listing prices are not confirmed transactions. See https://www.rareindex.io/data for methodology and terms.'; export interface Meta { count: number; cursor?: string | null; as_of: string; attribution: string; [k: string]: unknown; } export function envelope(data: T, meta: Partial & { count?: number } = {}) { const count = meta.count ?? (Array.isArray(data) ? data.length : 1); return { data, meta: { count, cursor: meta.cursor ?? null, as_of: new Date().toISOString(), attribution: ATTRIBUTION, ...meta } }; } export class ApiProblem extends Error { constructor( public status: number, public title: string, detail?: string, public type = 'about:blank', ) { super(detail ?? title); } } export function problem(reply: FastifyReply, status: number, title: string, detail?: string, extra: Record = {}) { return reply .code(status) .type('application/problem+json') .send({ type: 'https://www.rareindex.io/api-docs#errors', title, status, detail: detail ?? title, instance: reply.request.url, request_id: reply.request.id, ...extra }); } /** Opaque offset cursor. */ export function encodeCursor(offset: number): string { return Buffer.from(String(offset), 'utf8').toString('base64url'); } export function decodeCursor(cursor: string | undefined | null): number { if (!cursor) return 0; const n = Number(Buffer.from(cursor, 'base64url').toString('utf8')); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0; } /** Minimal CSV export for flat row arrays. */ export function toCsv(rows: Array>): string { if (rows.length === 0) return ''; const cols = [...new Set(rows.flatMap((r) => Object.keys(r)))]; const esc = (v: unknown) => { if (v === null || v === undefined) return ''; const s = v instanceof Date ? v.toISOString() : typeof v === 'object' ? JSON.stringify(v) : String(v); return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; return [cols.join(','), ...rows.map((r) => cols.map((c) => esc(r[c])).join(','))].join('\n') + '\n'; } export function wantsCsv(query: Record): boolean { return String(query.format ?? '').toLowerCase() === 'csv'; } export function sendList(reply: FastifyReply, rows: Array>, meta: Partial = {}, query: Record = {}) { if (wantsCsv(query)) return reply.type('text/csv; charset=utf-8').header('content-disposition', 'attachment; filename="rareindex-export.csv"').send(toCsv(rows)); return reply.send(envelope(rows, meta)); }