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.7 KB · 66 lines typescript
Raw Blame History
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