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%

certs: ri certs --backfill registers certificates + sightings from existing sales/listings

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 16 days ago (Sep 8, 2026) parent 928c8df

2 changed files +38 −3

modified workers/certs-verify.ts +34 −0
@@ -89,3 +89,37 @@ export async function verifyCertificates(opts: { limit?: number; grader?: string
89 89 log.info(res, 'certificate verification done');
90 90 return res;
91 91 }
92 +
93 +/**
94 + * One-off/idempotent backfill: register certificates + sightings from sales/listings/auction lots that already
95 + * carry a certification number (rows ingested before the certificates table existed). Safe to re-run.
96 + */
97 +export async function backfillCertificates(): Promise<{ certificates: number; sightings: number }> {
98 + const norm = (col: string) => sql.raw(`upper(regexp_replace(${col}, '[^A-Za-z0-9]', '', 'g'))`);
99 + const where = (tbl: string) => sql.raw(`${tbl}.certification_number is not null and ${tbl}.grader is not null and length(regexp_replace(${tbl}.certification_number, '[^A-Za-z0-9]', '', 'g')) between 5 and 20`);
100 + const inserted = (await db().execute(sql`
101 + with src as (
102 + select lower(s.grader) as grader, ${norm('s.certification_number')} as cert, s.asset_id, s.variant_id, s.grade, s.source_id, s.source_url, s.price_usd, s.sale_date as observed_at
103 + from sales s where ${where('s')}
104 + union all
105 + select lower(l.grader), ${norm('l.certification_number')}, l.asset_id, l.variant_id, l.grade, l.source_id, l.source_url, l.price_usd, l.last_seen_at
106 + from listings l where ${where('l')}
107 + ), agg as (
108 + select grader, cert, min(asset_id) as asset_id, min(variant_id) as variant_id, min(grade) as grade, min(observed_at) as first_seen, max(observed_at) as last_seen, count(*)::int as n,
109 + array_agg(distinct source_id) as sources, (array_agg(source_url order by observed_at desc))[1] as last_url, (array_agg(price_usd order by observed_at desc))[1] as last_price
110 + from src group by grader, cert
111 + )
112 + insert into certificates (id, grader, cert_number, asset_id, variant_id, grade, first_seen_at, last_seen_at, sightings, source_ids, last_source_url, last_price_usd)
113 + select 'cert_' || substr(md5(grader || '|' || cert), 1, 20), grader, cert, asset_id, variant_id, grade, first_seen, last_seen, n, sources, last_url, last_price from agg
114 + on conflict (grader, cert_number) do update set sightings = greatest(certificates.sightings, excluded.sightings), first_seen_at = least(certificates.first_seen_at, excluded.first_seen_at), last_seen_at = greatest(certificates.last_seen_at, excluded.last_seen_at), asset_id = coalesce(certificates.asset_id, excluded.asset_id)
115 + returning id`)) as unknown as unknown[];
116 + const sightings = (await db().execute(sql`
117 + insert into certificate_sightings (id, certificate_id, kind, target_id, source_id, connector_id, source_url, price_usd, currency, price, observed_at)
118 + select 'evt_' || substr(md5('sale' || s.id), 1, 20), c.id, 'sale', s.id, s.source_id, s.connector_id, s.source_url, s.price_usd, s.currency, s.price, s.sale_date
119 + from sales s join certificates c on c.grader = lower(s.grader) and c.cert_number = ${norm('s.certification_number')} where ${where('s')}
120 + union all
121 + select 'evt_' || substr(md5('listing' || l.id), 1, 20), c.id, 'listing', l.id, l.source_id, l.connector_id, l.source_url, l.price_usd, l.currency, l.price, l.last_seen_at
122 + from listings l join certificates c on c.grader = lower(l.grader) and c.cert_number = ${norm('l.certification_number')} where ${where('l')}
123 + on conflict do nothing returning id`)) as unknown as unknown[];
124 + return { certificates: inserted.length, sightings: sightings.length };
125 +}
modified workers/cli.ts +4 −3
@@ -3,7 +3,7 @@
3 3 * RareIndex operator CLI.
4 4 * ri connectors | ri crawl <id> [--mode probe|incremental|backfill] [--limit N]
5 5 * ri backfill <id> [--reset] [--pause] [--status] [--start YYYY-MM-DD] [--end YYYY-MM-DD]
6 − * ri certs [--limit N] [--grader psa] (verify cert numbers via cert_lookup connectors)
6 + * ri certs [--limit N] [--grader psa] | ri certs --backfill (verify cert numbers via cert_lookup connectors / register certs from existing sales+listings)
7 7 * ri normalize [--connector id] [--limit N] | ri resolve [--limit N]
8 8 * ri value [--asset id | --all] [--history] | ri premiums [--category slug]
9 9 * ri index | ri snapshots | ri radar | ri health [--probe] | ri fx [--backfill] | ri benchmarks
@@ -28,7 +28,7 @@ import { syncBenchmarks } from './benchmarks.ts';
28 28 import { computeHealth } from './health.ts';
29 29 import { expireListings } from './listings-expire.ts';
30 30 import { imageStats, processImages } from './image-processing/index.ts';
31 −import { verifyCertificates } from './certs-verify.ts';
31 +import { backfillCertificates, verifyCertificates } from './certs-verify.ts';
32 32
33 33 const [cmd = 'help', ...rest] = process.argv.slice(2);
34 34 const flags: Record<string, string | boolean> = {};
@@ -103,7 +103,8 @@ async function main() {
103 103 break;
104 104 }
105 105 case 'certs':
106 − print(await verifyCertificates({ limit: num('limit', 100), grader: str('grader') }));
106 + if (flags.backfill) print(await backfillCertificates());
107 + else print(await verifyCertificates({ limit: num('limit', 100), grader: str('grader') }));
107 108 break;
108 109 case 'normalize':
109 110 print({ processed: await drain(() => normalizeBatch({ connectorId: str('connector'), limit: num('limit', 500) }), num('limit', 500)) });
110 111