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.1 KB · 98 lines typescript
Raw Blame History
1import { pgTable, text, integer, index, uniqueIndex, jsonb } from 'drizzle-orm/pg-core';2import { createdAt, ts, ratio } from './_common.js';34/**5 * Immutable raw capture (§108, §142). Rows are never updated except `processed_at`/`process_error`6 * bookkeeping columns. Payload is the parsed source payload (JSON from API, or extracted fields);7 * large HTML snapshots live in RI_DATA_DIR/raw/<connector>/<hash>.html referenced by snapshot_ref.8 */9export const rawRecords = pgTable(10  'raw_records',11  {12    id: text('id').primaryKey(),13    connectorId: text('connector_id').notNull(),14    sourceId: text('source_id').notNull(),15    runId: text('run_id'),16    engine: text('engine').notNull(),17    url: text('url').notNull(),18    externalId: text('external_id'),19    kind: text('kind').notNull(),20    fetchedAt: ts('fetched_at').notNull(),21    contentHash: text('content_hash').notNull(),22    httpStatus: integer('http_status'),23    payload: jsonb('payload').notNull(),24    snapshotRef: text('snapshot_ref'),25    parserVersion: text('parser_version').notNull(),26    connectorVersion: text('connector_version').notNull(),27    processedAt: ts('processed_at'),28    processError: text('process_error'),29    createdAt: createdAt(),30  },31  (t) => [32    uniqueIndex('raw_records_connector_hash_uq').on(t.connectorId, t.contentHash),33    index('raw_records_connector_fetched_idx').on(t.connectorId, t.fetchedAt),34    index('raw_records_unprocessed_idx').on(t.processedAt),35    index('raw_records_external_idx').on(t.connectorId, t.externalId),36  ],37);3839/** Staging between normalisation and canonical tables; keeps the match decision auditable (§112). */40export const normalizedRecords = pgTable(41  'normalized_records',42  {43    id: text('id').primaryKey(),44    rawRecordId: text('raw_record_id').notNull(),45    connectorId: text('connector_id').notNull(),46    sourceId: text('source_id').notNull(),47    kind: text('kind').notNull(),48    payload: jsonb('payload').notNull(),49    /** position of this record within the output of normalize(raw) — one raw row may yield several records */50    seq: integer('seq').notNull().default(0),51    assetId: text('asset_id'),52    variantId: text('variant_id'),53    matchMethod: text('match_method'), // identifier | canonical_key | fuzzy | embedding | llm | manual54    matchConfidence: ratio('match_confidence'),55    status: text('status').notNull().default('pending'), // pending | matched | unmatched | rejected | applied56    rejectReason: text('reject_reason'),57    targetId: text('target_id'), // resulting sale/listing/observation id58    createdAt: createdAt(),59    processedAt: ts('processed_at'),60  },61  (t) => [62    index('normalized_records_status_idx').on(t.status, t.createdAt),63    index('normalized_records_status_kind_idx').on(t.status, t.kind, t.createdAt),64    index('normalized_records_asset_idx').on(t.assetId),65    uniqueIndex('normalized_records_raw_uq').on(t.rawRecordId, t.kind, t.seq),66  ],67);6869/** Pipeline/event log (§140). Queue itself is pg-boss; this is the durable domain event stream. */70export const events = pgTable(71  'events',72  {73    id: text('id').primaryKey(),74    type: text('type').notNull(), // page_discovered | page_crawled | listing_created | listing_updated | sale_detected | entity_matched | valuation_updated | index_updated75    entityType: text('entity_type'),76    entityId: text('entity_id'),77    payload: jsonb('payload').$type<Record<string, unknown>>().notNull().default({}),78    createdAt: createdAt(),79  },80  (t) => [index('events_type_created_idx').on(t.type, t.createdAt), index('events_entity_idx').on(t.entityType, t.entityId)],81);8283/** Audit log — never silently delete or alter data (§116). */84export const auditLog = pgTable(85  'audit_log',86  {87    id: text('id').primaryKey(),88    entityType: text('entity_type').notNull(),89    entityId: text('entity_id').notNull(),90    action: text('action').notNull(), // flagged | excluded | restored | merged | edited | rejected91    reason: text('reason').notNull(),92    actor: text('actor').notNull().default('system'),93    details: jsonb('details').$type<Record<string, unknown>>().notNull().default({}),94    createdAt: createdAt(),95  },96  (t) => [index('audit_entity_idx').on(t.entityType, t.entityId)],97);98