/** * Live capture + smoke for the luxury-wave connectors. Runs a probe crawl through the real router, * prints normalised samples and saves the first raw record(s) as fixtures. * * pnpm tsx connectors/api/_luxury-lib/capture.ts [limit=1] [--no-save] */ import { readFileSync, existsSync } from 'node:fs'; import path from 'node:path'; import { createCrawlContext, createRouter, ConnectorMetaSchema } from '@rareindex/connectors'; import { saveFixture } from '@rareindex/connectors/testing'; import type { RareIndexConnector, ConnectorMeta } from '@rareindex/connectors'; function loadEnv() { const p = path.resolve(process.cwd(), '.env'); if (!existsSync(p)) return; for (const line of readFileSync(p, 'utf8').split('\n')) { const m = line.match(/^([A-Z0-9_]+)=(.*)$/); if (m && !process.env[m[1]!]) process.env[m[1]!] = m[2]!.replace(/^"(.*)"$/, '$1'); } } async function main() { loadEnv(); const id = process.argv[2]; if (!id) throw new Error('usage: capture.ts [limit] [--no-save]'); const limit = Number(process.argv[3] ?? 1); const save = !process.argv.includes('--no-save'); const dirs = ['api', 'firecrawl', 'scrapfly']; const dir = dirs.map((d) => path.resolve('connectors', d, id)).find((d) => existsSync(path.join(d, 'meta.json'))); if (!dir) throw new Error(`no meta.json for ${id}`); const metaRaw = JSON.parse(readFileSync(path.join(dir, 'meta.json'), 'utf8')); const meta: ConnectorMeta = ConnectorMetaSchema.parse({ ...metaRaw, module: metaRaw.module ?? `${path.basename(path.dirname(dir))}/${id}` }); const mod = (await import(path.join(dir, 'index.ts'))) as { default: (m: ConnectorMeta) => RareIndexConnector }; const connector = mod.default(meta); const router = createRouter({ firecrawlApiKey: process.env.FIRECRAWL_API_KEY, scrapflyApiKey: process.env.SCRAPFLY_API_KEY }); const ctx = createCrawlContext({ router, meta, options: { mode: 'probe', limit } }); let n = 0; for await (const raw of connector.crawl(ctx)) { n++; const records = await connector.normalize({ ...raw, externalId: raw.externalId ?? null, fetchedAt: raw.fetchedAt ?? new Date() }); const kinds = records.reduce>((acc, r) => ((acc[r.kind] = (acc[r.kind] ?? 0) + 1), acc), {}); console.log(`\n[${id}] raw #${n} ${raw.url} engine=${raw.engine} → ${records.length} records ${JSON.stringify(kinds)}`); for (const r of records.slice(0, 3)) { const anyR = r as Record; console.log(' -', r.kind, '|', (anyR.rawTitle as string | undefined)?.slice(0, 90) ?? (anyR.title as string | undefined), '|', 'price' in r ? `${(r as { price: unknown }).price} ${(r as { currency?: string }).currency ?? ''}` : '', '|', 'saleDate' in r ? (r as { saleDate: Date }).saleDate.toISOString().slice(0, 10) : '', '|', 'attributes' in r ? JSON.stringify({ cat: r.attributes.categorySlug, brand: r.attributes.brand, model: r.attributes.model, ref: r.attributes.reference, ids: r.attributes.identifiers }).slice(0, 220) : ''); } if (save) { const name = `${(raw.externalId ?? `raw-${n}`).replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase()}`; saveFixture(id, name, { raw: { url: raw.url, externalId: raw.externalId ?? null, kind: raw.kind, engine: raw.engine, fetchedAt: raw.fetchedAt ?? new Date(), payload: raw.payload }, expect: { minCount: Math.min(records.length, 1), kinds: [...new Set(records.map((r) => r.kind))] }, note: 'Captured live by connectors/api/_luxury-lib/capture.ts' }); console.log(` saved fixture ${name}`); } } console.log(`\n[${id}] done: ${n} raw records; engine stats ${JSON.stringify(ctx.engineStats)}; anomalies ${JSON.stringify(ctx.anomalies)}`); } main().catch((e) => { console.error(e); process.exit(1); });