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