TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { FastifyReply } from 'fastify';23export 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.';45export interface Meta {6 count: number;7 cursor?: string | null;8 as_of: string;9 attribution: string;10 [k: string]: unknown;11}1213export function envelope<T>(data: T, meta: Partial<Meta> & { count?: number } = {}) {14 const count = meta.count ?? (Array.isArray(data) ? data.length : 1);15 return { data, meta: { count, cursor: meta.cursor ?? null, as_of: new Date().toISOString(), attribution: ATTRIBUTION, ...meta } };16}1718export class ApiProblem extends Error {19 constructor(20 public status: number,21 public title: string,22 detail?: string,23 public type = 'about:blank',24 ) {25 super(detail ?? title);26 }27}2829export function problem(reply: FastifyReply, status: number, title: string, detail?: string, extra: Record<string, unknown> = {}) {30 return reply31 .code(status)32 .type('application/problem+json')33 .send({ type: 'https://www.rareindex.io/api-docs#errors', title, status, detail: detail ?? title, instance: reply.request.url, request_id: reply.request.id, ...extra });34}3536/** Opaque offset cursor. */37export function encodeCursor(offset: number): string {38 return Buffer.from(String(offset), 'utf8').toString('base64url');39}40export function decodeCursor(cursor: string | undefined | null): number {41 if (!cursor) return 0;42 const n = Number(Buffer.from(cursor, 'base64url').toString('utf8'));43 return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;44}4546/** Minimal CSV export for flat row arrays. */47export function toCsv(rows: Array<Record<string, unknown>>): string {48 if (rows.length === 0) return '';49 const cols = [...new Set(rows.flatMap((r) => Object.keys(r)))];50 const esc = (v: unknown) => {51 if (v === null || v === undefined) return '';52 const s = v instanceof Date ? v.toISOString() : typeof v === 'object' ? JSON.stringify(v) : String(v);53 return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;54 };55 return [cols.join(','), ...rows.map((r) => cols.map((c) => esc(r[c])).join(','))].join('\n') + '\n';56}5758export function wantsCsv(query: Record<string, unknown>): boolean {59 return String(query.format ?? '').toLowerCase() === 'csv';60}6162export function sendList(reply: FastifyReply, rows: Array<Record<string, unknown>>, meta: Partial<Meta> = {}, query: Record<string, unknown> = {}) {63 if (wantsCsv(query)) return reply.type('text/csv; charset=utf-8').header('content-disposition', 'attachment; filename="rareindex-export.csv"').send(toCsv(rows));64 return reply.send(envelope(rows, meta));65}66