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%
5.0 KB · 83 lines typescript
Raw Blame History
1/**2 * Build fixtures for the cert connectors from LIVE public verification pages (through the real router,3 * i.e. Firecrawl → Scrapfly exactly as production does). Every fixture is a real capture, trimmed by4 * the connector's own `trim`.5 *6 *   pnpm tsx connectors/firecrawl/_g2-grading-lib/capture.ts <connectorId> <fixtureName>=<certOrUrl> [...]7 *   pnpm tsx connectors/firecrawl/_g2-grading-lib/capture.ts psa-cert charizard-psa10=69211238 not-found=999999998 *9 * Also usable as a live smoke: prints the normalised records of each capture.10 */11import { existsSync, readFileSync, readdirSync } from 'node:fs';12import path from 'node:path';13import { createCrawlContext, createRouter, ConnectorMetaSchema, DomainsFileSchema, DOMAINS_DIR, DOMAINS_PATH, setDomains } from '@rareindex/connectors';14import { saveFixture } from '@rareindex/connectors/testing';15import { childLogger } from '@rareindex/shared';16import type { CertLookupConnector } from './cert-base.js';1718const [connectorId, ...pairs] = process.argv.slice(2);19if (!connectorId || !pairs.length) {20  console.error('usage: capture.ts <connectorId> <fixtureName>=<certOrUrl> ...');21  process.exit(1);22}23try {24  process.loadEnvFile(path.resolve('.env'));25} catch {26  /* .env optional */27}2829// Dev-time guard: while several groups edit connectors/domains.d in parallel, one invalid fragment must30// not stop our captures — load domains.json + every fragment that validates, warn about the others.31{32  const base = DomainsFileSchema.parse(JSON.parse(readFileSync(DOMAINS_PATH, 'utf8')));33  if (existsSync(DOMAINS_DIR)) {34    for (const f of readdirSync(DOMAINS_DIR).filter((x) => x.endsWith('.json')).sort()) {35      const parsed = DomainsFileSchema.safeParse({ ...JSON.parse(readFileSync(path.join(DOMAINS_DIR, f), 'utf8')), version: base.version });36      if (!parsed.success) {37        console.warn(`  [capture] skipping invalid domains fragment ${f}: ${parsed.error.issues[0]?.path.join('.')} ${parsed.error.issues[0]?.message}`);38        continue;39      }40      for (const [host, patch] of Object.entries(parsed.data.domains)) base.domains[host] = { ...(base.domains[host] ?? {}), ...patch };41    }42  }43  setDomains(base);44}4546const dir = path.resolve('connectors/firecrawl', connectorId);47const meta = ConnectorMetaSchema.parse(JSON.parse(readFileSync(path.join(dir, 'meta.json'), 'utf8')));48const mod = (await import(path.join(dir, 'index.ts'))) as { default: (m: typeof meta) => CertLookupConnector };49const connector = mod.default(meta);50const router = createRouter({ firecrawlApiKey: process.env.FIRECRAWL_API_KEY, scrapflyApiKey: process.env.SCRAPFLY_API_KEY });51const ctx = createCrawlContext({ router, meta, options: { mode: 'probe', limit: pairs.length }, log: childLogger({ connector: connectorId, level: 'warn' }) });5253for (const pair of pairs) {54  const eq = pair.indexOf('=');55  const name = pair.slice(0, eq);56  const seed = pair.slice(eq + 1);57  const cert = /^https?:/.test(seed) ? connector.certFromUrl(seed) : seed;58  if (!cert) {59    console.error(`  [${connectorId}] ${name}: cannot derive a cert from ${seed}`);60    continue;61  }62  const raw = await connector.fetchCert(ctx, cert, { includeNotFound: true });63  if (!raw) {64    console.error(`  [${connectorId}] ${name}: no page captured (${JSON.stringify(ctx.anomalies.slice(-1))})`);65    continue;66  }67  const out = await connector.normalize({ ...raw, externalId: raw.externalId ?? null, fetchedAt: raw.fetchedAt ?? new Date() });68  const payload = raw.payload as { status: string; url: string };69  const kinds = [...new Set(out.map((r) => r.kind))];70  saveFixture(connectorId, name, {71    raw: { url: raw.url, externalId: raw.externalId ?? null, kind: raw.kind, engine: raw.engine, fetchedAt: raw.fetchedAt ?? new Date(), payload: raw.payload },72    // requiredFields must hold for every record kind (population_report has no `grade` block).73    expect: payload.status === 'not_found' ? { count: 0 } : { count: out.length, kinds, requiredFields: ['attributes.categorySlug', `attributes.identifiers.${connector.idKey}`], first: { 'grade.certificationNumber': connector.certIdentifier(cert) } },74    note: payload.status === 'not_found' ? `Live capture (${raw.engine}) of ${payload.url}: the grader reports this certification number as not found; normalises to zero records.` : `Live capture (${raw.engine}) of the public verification page ${payload.url}, trimmed to the results fragment.`,75  });76  console.log(`  [${connectorId}] ${name}: ${payload.status} via ${raw.engine} → ${out.length} record(s) ${kinds.join(',')}`);77  for (const r of out) {78    if (r.kind === 'catalog_item') console.log(`     ${r.rawTitle} | ${r.attributes.categorySlug} | grade=${r.grade.grader} ${r.grade.grade ?? '-'}${r.grade.qualifier ? ` (${r.grade.qualifier})` : ''} | ids=${JSON.stringify(r.attributes.identifiers)}`);79    if (r.kind === 'population_report') console.log(`     population total=${r.total} byGrade=${JSON.stringify(r.byGrade)}`);80  }81}82console.log(`[${connectorId}] engineStats=${JSON.stringify(ctx.engineStats)} anomalies=${ctx.anomalies.length}`);83