import { z } from 'zod'; import { AssetAttributesSchema, NormalizedCatalogItemSchema, NormalizedPriceObservationSchema, type NormalizedCatalogItem, type NormalizedPriceObservation } from '@rareindex/shared'; /** Helpers shared by the official-API connectors (kept inside connectors/api, not the framework). */ export type AttrInput = z.input; export function attrs(input: AttrInput) { return AssetAttributesSchema.parse(input); } export function catalogItem(input: z.input): NormalizedCatalogItem { return NormalizedCatalogItemSchema.parse(input); } export function priceObservation(input: z.input): NormalizedPriceObservation { return NormalizedPriceObservationSchema.parse(input); } /** "2026/09/06" | "2026-09-06" | "2022/10/10 15:12:00" → UTC Date, else null. */ export function parseSlashDate(s: string | null | undefined): Date | null { if (!s) return null; const m = s.match(/^(\d{4})[/-](\d{2})[/-](\d{2})/); if (!m) return null; const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))); return Number.isNaN(d.getTime()) ? null : d; } export function yearOf(s: string | null | undefined): number | null { const d = parseSlashDate(s); return d ? d.getUTCFullYear() : null; } /** Parse a numeric price from string/number; null when missing, non-finite or ≤ 0. */ export function num(v: unknown): number | null { if (v === null || v === undefined || v === '') return null; const n = typeof v === 'number' ? v : Number.parseFloat(String(v)); return Number.isFinite(n) && n > 0 ? n : null; } /** "Charizard · Base Set #4/102 (1999)" style title. */ export function makeTitle(parts: { name: string; set?: string | null; number?: string | null; total?: number | string | null; year?: number | null; variant?: string | null }): string { let t = parts.name; const loc: string[] = []; if (parts.set) loc.push(parts.set); if (parts.number) loc.push(`#${parts.number}${parts.total ? `/${parts.total}` : ''}`); if (loc.length) t += ` · ${loc.join(' ')}`; if (parts.variant) t += ` ${parts.variant}`; if (parts.year) t += ` (${parts.year})`; return t; } /** Generic retry wrapper for flaky public APIs (in addition to the HTTP engine's own retries). */ export async function withRetries(fn: () => Promise, isOk: (v: T) => boolean, attempts = 4, baseDelayMs = 1500): Promise { let last!: T; for (let i = 0; i < attempts; i++) { last = await fn(); if (isOk(last)) return last; await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** i)); } return last; }