/** * Build fixtures for the official-API connectors from live captures. * Usage: pnpm tsx connectors/api/_lib/capture.ts (fetches live; keeps fixtures small) */ import { saveFixture } from '@rareindex/connectors/testing'; import { trimCard as trimScryfall } from '../scryfall/index.js'; import { trimCard as trimPokemon } from '../pokemontcg/index.js'; import { trimCard as trimYgo } from '../ygoprodeck/index.js'; const UA = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)', accept: 'application/json' }; async function getJson(url: string, attempts = 5): Promise { let last = ''; for (let i = 0; i < attempts; i++) { const res = await fetch(url, { headers: UA }); if (res.ok) return (await res.json()) as T; last = `HTTP ${res.status}`; await new Promise((r) => setTimeout(r, 1500 * 2 ** i)); } throw new Error(`${url}: ${last}`); } const fetchedAt = new Date().toISOString(); // ---- Scryfall: single cards via API (same shape as bulk lines) ---- const scry: Array<[string, string, Record]> = [ ['lea/232', 'black-lotus-alpha', { count: 2, kinds: ['catalog_item', 'price_observation'], 'first.attributes.setCode': 'LEA' }], ['sld/1145', 'secret-lair-foil-nonfoil', { minCount: 4 }], ['neo/430', 'neo-showcase-foil-only', { minCount: 2 }], ['clb/682', 'clb-double-faced', { minCount: 2 }], ]; for (const [path, name, expect] of scry) { const card = await getJson>(`https://api.scryfall.com/cards/${path}`); await new Promise((r) => setTimeout(r, 120)); saveFixture('scryfall', name, { 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' } }, 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'] }, note: `Live capture of https://api.scryfall.com/cards/${path}; bulkUpdatedAt copied from the bulk-data listing of the same day.`, }); } // ---- Pokémon TCG: base1 (API is flaky → retries) ---- const pk = await getJson<{ data: Record[] }>('https://api.pokemontcg.io/v2/cards?q=set.id:base1'); const wanted = new Set(['base1-1', 'base1-4', 'base1-58', 'base1-102']); for (const c of pk.data.filter((x) => wanted.has(String(x.id)))) { const card = trimPokemon(c); saveFixture('pokemontcg', String(card.id), { 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' } }, expect: { minCount: 1, kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.pokemontcg_id', 'attributes.set', 'attributes.number', 'attributes.year'] }, note: 'Live capture of https://api.pokemontcg.io/v2/cards?q=set.id:base1 (trimmed).', }); } // GitHub mirror shape (no prices) const mirror = await getJson[]>('https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master/cards/en/base1.json'); const sets = await getJson[]>('https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master/sets/en.json'); const base1 = sets.find((s) => s.id === 'base1') as Record; const m4 = mirror.find((c) => c.id === 'base1-4')!; saveFixture('pokemontcg', 'base1-4-mirror', { 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' } }, expect: { count: 1, kinds: ['catalog_item'], first: { 'attributes.variant': null } }, note: 'GitHub mirror fallback (PokemonTCG/pokemon-tcg-data): catalog only, no prices.', }); // ---- YGOPRODeck ---- const ygo = await getJson<{ data: Record[] }>('https://db.ygoprodeck.com/api/v7/cardinfo.php?name=Dark%20Magician%7CBlue-Eyes%20White%20Dragon%7CPot%20of%20Greed&misc=yes'); for (const c of ygo.data) { const card = trimYgo(c); saveFixture('ygoprodeck', String(card.id), { raw: { url: String(card.ygoprodeck_url), externalId: String(card.id), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(fetchedAt), payload: { card } }, expect: { minCount: (card.card_sets?.length ?? 1), kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.ygo_id', 'attributes.set', 'attributes.number'] }, note: 'Live capture of cardinfo.php?name=…&misc=yes (trimmed).', }); } console.log('fixtures written');