TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import { z } from 'zod';2import { AssetAttributesSchema, NormalizedCatalogItemSchema, NormalizedPriceObservationSchema, type NormalizedCatalogItem, type NormalizedPriceObservation } from '@rareindex/shared';34/** Helpers shared by the official-API connectors (kept inside connectors/api, not the framework). */56export type AttrInput = z.input<typeof AssetAttributesSchema>;78export function attrs(input: AttrInput) {9 return AssetAttributesSchema.parse(input);10}1112export function catalogItem(input: z.input<typeof NormalizedCatalogItemSchema>): NormalizedCatalogItem {13 return NormalizedCatalogItemSchema.parse(input);14}1516export function priceObservation(input: z.input<typeof NormalizedPriceObservationSchema>): NormalizedPriceObservation {17 return NormalizedPriceObservationSchema.parse(input);18}1920/** "2026/09/06" | "2026-09-06" | "2022/10/10 15:12:00" → UTC Date, else null. */21export function parseSlashDate(s: string | null | undefined): Date | null {22 if (!s) return null;23 const m = s.match(/^(\d{4})[/-](\d{2})[/-](\d{2})/);24 if (!m) return null;25 const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));26 return Number.isNaN(d.getTime()) ? null : d;27}2829export function yearOf(s: string | null | undefined): number | null {30 const d = parseSlashDate(s);31 return d ? d.getUTCFullYear() : null;32}3334/** Parse a numeric price from string/number; null when missing, non-finite or ≤ 0. */35export function num(v: unknown): number | null {36 if (v === null || v === undefined || v === '') return null;37 const n = typeof v === 'number' ? v : Number.parseFloat(String(v));38 return Number.isFinite(n) && n > 0 ? n : null;39}4041/** "Charizard · Base Set #4/102 (1999)" style title. */42export function makeTitle(parts: { name: string; set?: string | null; number?: string | null; total?: number | string | null; year?: number | null; variant?: string | null }): string {43 let t = parts.name;44 const loc: string[] = [];45 if (parts.set) loc.push(parts.set);46 if (parts.number) loc.push(`#${parts.number}${parts.total ? `/${parts.total}` : ''}`);47 if (loc.length) t += ` · ${loc.join(' ')}`;48 if (parts.variant) t += ` ${parts.variant}`;49 if (parts.year) t += ` (${parts.year})`;50 return t;51}5253/** Generic retry wrapper for flaky public APIs (in addition to the HTTP engine's own retries). */54export async function withRetries<T>(fn: () => Promise<T>, isOk: (v: T) => boolean, attempts = 4, baseDelayMs = 1500): Promise<T> {55 let last!: T;56 for (let i = 0; i < attempts; i++) {57 last = await fn();58 if (isOk(last)) return last;59 await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** i));60 }61 return last;62}63