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%
4.7 KB · 75 lines typescript
Raw Blame History
1/**2 * Build fixtures for the official-API connectors from live captures.3 * Usage: pnpm tsx connectors/api/_lib/capture.ts   (fetches live; keeps fixtures small)4 */5import { saveFixture } from '@rareindex/connectors/testing';6import { trimCard as trimScryfall } from '../scryfall/index.js';7import { trimCard as trimPokemon } from '../pokemontcg/index.js';8import { trimCard as trimYgo } from '../ygoprodeck/index.js';910const UA = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)', accept: 'application/json' };1112async function getJson<T>(url: string, attempts = 5): Promise<T> {13  let last = '';14  for (let i = 0; i < attempts; i++) {15    const res = await fetch(url, { headers: UA });16    if (res.ok) return (await res.json()) as T;17    last = `HTTP ${res.status}`;18    await new Promise((r) => setTimeout(r, 1500 * 2 ** i));19  }20  throw new Error(`${url}: ${last}`);21}2223const fetchedAt = new Date().toISOString();2425// ---- Scryfall: single cards via API (same shape as bulk lines) ----26const scry: Array<[string, string, Record<string, unknown>]> = [27  ['lea/232', 'black-lotus-alpha', { count: 2, kinds: ['catalog_item', 'price_observation'], 'first.attributes.setCode': 'LEA' }],28  ['sld/1145', 'secret-lair-foil-nonfoil', { minCount: 4 }],29  ['neo/430', 'neo-showcase-foil-only', { minCount: 2 }],30  ['clb/682', 'clb-double-faced', { minCount: 2 }],31];32for (const [path, name, expect] of scry) {33  const card = await getJson<Record<string, unknown>>(`https://api.scryfall.com/cards/${path}`);34  await new Promise((r) => setTimeout(r, 120));35  saveFixture('scryfall', name, {36    raw: { url: String(card.scryfall_uri), externalId: String(card.id), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(fetchedAt), payload: { card: trimScryfall(card), bulkUpdatedAt: '2026-09-06T21:05:43.673+00:00' } },37    expect: { count: (expect.count as number | undefined), minCount: (expect.minCount as number | undefined), kinds: expect.kinds as string[] | undefined, requiredFields: ['attributes.identifiers.scryfall_id', 'attributes.setCode', 'attributes.number'] },38    note: `Live capture of https://api.scryfall.com/cards/${path}; bulkUpdatedAt copied from the bulk-data listing of the same day.`,39  });40}4142// ---- Pokémon TCG: base1 (API is flaky → retries) ----43const pk = await getJson<{ data: Record<string, unknown>[] }>('https://api.pokemontcg.io/v2/cards?q=set.id:base1');44const wanted = new Set(['base1-1', 'base1-4', 'base1-58', 'base1-102']);45for (const c of pk.data.filter((x) => wanted.has(String(x.id)))) {46  const card = trimPokemon(c);47  saveFixture('pokemontcg', String(card.id), {48    raw: { url: `https://api.pokemontcg.io/v2/cards/${card.id}`, externalId: card.id, kind: 'catalog_item', engine: 'api', fetchedAt: new Date(fetchedAt), payload: { card, source: 'api' } },49    expect: { minCount: 1, kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.pokemontcg_id', 'attributes.set', 'attributes.number', 'attributes.year'] },50    note: 'Live capture of https://api.pokemontcg.io/v2/cards?q=set.id:base1 (trimmed).',51  });52}53// GitHub mirror shape (no prices)54const mirror = await getJson<Record<string, unknown>[]>('https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master/cards/en/base1.json');55const sets = await getJson<Record<string, unknown>[]>('https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master/sets/en.json');56const base1 = sets.find((s) => s.id === 'base1') as Record<string, unknown>;57const m4 = mirror.find((c) => c.id === 'base1-4')!;58saveFixture('pokemontcg', 'base1-4-mirror', {59  raw: { url: 'https://api.pokemontcg.io/v2/cards/base1-4', externalId: 'base1-4', kind: 'catalog_item', engine: 'feed', fetchedAt: new Date(fetchedAt), payload: { card: trimPokemon(m4, base1 as never), source: 'github-mirror' } },60  expect: { count: 1, kinds: ['catalog_item'], first: { 'attributes.variant': null } },61  note: 'GitHub mirror fallback (PokemonTCG/pokemon-tcg-data): catalog only, no prices.',62});6364// ---- YGOPRODeck ----65const ygo = await getJson<{ data: Record<string, unknown>[] }>('https://db.ygoprodeck.com/api/v7/cardinfo.php?name=Dark%20Magician%7CBlue-Eyes%20White%20Dragon%7CPot%20of%20Greed&misc=yes');66for (const c of ygo.data) {67  const card = trimYgo(c);68  saveFixture('ygoprodeck', String(card.id), {69    raw: { url: String(card.ygoprodeck_url), externalId: String(card.id), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(fetchedAt), payload: { card } },70    expect: { minCount: (card.card_sets?.length ?? 1), kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.ygo_id', 'attributes.set', 'attributes.number'] },71    note: 'Live capture of cardinfo.php?name=…&misc=yes (trimmed).',72  });73}74console.log('fixtures written');75