SPB Git

spb/rareindex

Public
8.7 KB · 217 lines typescript
1import { pgTable, text, integer, boolean, real, index, uniqueIndex, jsonb, vector } from 'drizzle-orm/pg-core';2import { sql } from 'drizzle-orm';3import { createdAt, updatedAt, ts, money, ratio, jsonObject, textArray, tsvector } from './_common.js';45/**6 * Canonical asset (§109): the collectible object independent of grade/condition.7 * `canonical_key` is a deterministic normalised key (category|set|number|name|variant|language|year…)8 * used for entity resolution; `identifiers` hold deterministic external ids.9 */10export const assets = pgTable(11  'assets',12  {13    id: text('id').primaryKey(),14    slug: text('slug').notNull(),15    canonicalKey: text('canonical_key').notNull(),16    categorySlug: text('category_slug').notNull(),17    subcategorySlug: text('subcategory_slug'),18    familySlug: text('family_slug').notNull(),19    franchise: text('franchise'),20    brand: text('brand'),21    series: text('series'),22    setSlug: text('set_slug'),23    setName: text('set_name'),24    setCode: text('set_code'),25    name: text('name').notNull(),26    /** full display title, e.g. "Charizard #4/102 — Base Set 1st Edition Holo (1999)" */27    title: text('title').notNull(),28    model: text('model'),29    reference: text('reference'),30    number: text('number'),31    year: integer('year'),32    edition: text('edition'),33    variant: text('variant'),34    language: text('language'),35    region: text('region'),36    country: text('country'),37    material: text('material'),38    size: text('size'),39    color: text('color'),40    rarity: text('rarity'),41    productionQuantity: integer('production_quantity'),42    originalMsrp: money('original_msrp'),43    originalMsrpCurrency: text('original_msrp_currency'),44    releaseDate: text('release_date'),45    description: text('description'),46    heroImageUrl: text('hero_image_url'),47    identifiers: jsonObject<Record<string, string>>('identifiers'),48    metadata: jsonObject<Record<string, unknown>>('metadata'),49    /** ids of assets merged into this one (audit trail) */50    mergedFrom: textArray('merged_from'),51    /** 0–100 (§150) */52    dataQuality: real('data_quality').notNull().default(0),53    verified: boolean('verified').notNull().default(false),54    search: tsvector('search').generatedAlwaysAs(55      (): ReturnType<typeof sql> =>56        sql`setweight(to_tsvector('simple', coalesce(name, '')), 'A') || setweight(to_tsvector('simple', coalesce(set_name, '') || ' ' || coalesce(number, '') || ' ' || coalesce(variant, '') || ' ' || coalesce(edition, '')), 'B') || setweight(to_tsvector('simple', coalesce(brand, '') || ' ' || coalesce(franchise, '') || ' ' || coalesce(reference, '') || ' ' || coalesce(model, '') || ' ' || coalesce(year::text, '') || ' ' || coalesce(category_slug, '')), 'C')`,57    ),58    createdAt: createdAt(),59    updatedAt: updatedAt(),60  },61  (t) => [62    uniqueIndex('assets_canonical_key_uq').on(t.canonicalKey),63    uniqueIndex('assets_slug_uq').on(t.slug),64    index('assets_category_idx').on(t.categorySlug),65    index('assets_family_idx').on(t.familySlug),66    index('assets_set_idx').on(t.setSlug),67    index('assets_search_gin').using('gin', t.search),68    index('assets_title_trgm').using('gin', sql`${t.title} gin_trgm_ops`),69    index('assets_identifiers_gin').using('gin', t.identifiers),70  ],71);7273/** Grade/condition-specific variant of an asset (PSA 10, CIB, deadstock size 10…) — §112, §117, §118. */74export const assetVariants = pgTable(75  'asset_variants',76  {77    id: text('id').primaryKey(),78    assetId: text('asset_id').notNull(),79    variantKey: text('variant_key').notNull(), // e.g. "psa|10", "raw|near_mint", "sealed", "bgs|9.5|black_label", "size|10"80    grader: text('grader'),81    grade: text('grade'),82    qualifier: text('qualifier'),83    condition: text('condition'),84    completeness: text('completeness'),85    sizeLabel: text('size_label'),86    label: text('label').notNull(), // human label "PSA 10"87    isDefault: boolean('is_default').notNull().default(false),88    createdAt: createdAt(),89  },90  (t) => [uniqueIndex('asset_variants_uq').on(t.assetId, t.variantKey), index('asset_variants_asset_idx').on(t.assetId)],91);9293/** Denormalised, frequently read metrics per asset (rebuilt by valuation/indices workers). */94export const assetStats = pgTable(95  'asset_stats',96  {97    assetId: text('asset_id').primaryKey(),98    rivUsd: money('riv_usd'),99    rivLowUsd: money('riv_low_usd'),100    rivHighUsd: money('riv_high_usd'),101    rivConfidence: ratio('riv_confidence'),102    rivSampleSize: integer('riv_sample_size').notNull().default(0),103    rivVariantId: text('riv_variant_id'),104    latestSaleUsd: money('latest_sale_usd'),105    latestSaleAt: ts('latest_sale_at'),106    change1d: ratio('change_1d'),107    change7d: ratio('change_7d'),108    change30d: ratio('change_30d'),109    change90d: ratio('change_90d'),110    change1y: ratio('change_1y'),111    athUsd: money('ath_usd'),112    athAt: ts('ath_at'),113    atlUsd: money('atl_usd'),114    atlAt: ts('atl_at'),115    salesCount: integer('sales_count').notNull().default(0),116    sales30d: integer('sales_30d').notNull().default(0),117    sales1y: integer('sales_1y').notNull().default(0),118    volume30dUsd: money('volume_30d_usd'),119    activeListings: integer('active_listings').notNull().default(0),120    minAskUsd: money('min_ask_usd'),121    observationsCount: integer('observations_count').notNull().default(0),122    sourcesCount: integer('sources_count').notNull().default(0),123    liquidityScore: real('liquidity_score'),124    rarityScore: real('rarity_score'),125    momentum7d: real('momentum_7d'),126    momentum30d: real('momentum_30d'),127    momentum90d: real('momentum_90d'),128    momentum1y: real('momentum_1y'),129    trendingScore: real('trending_score'),130    valueOpportunity: real('value_opportunity'),131    dataQuality: real('data_quality'),132    watchers: integer('watchers').notNull().default(0),133    views30d: integer('views_30d').notNull().default(0),134    updatedAt: updatedAt(),135  },136  (t) => [index('asset_stats_riv_idx').on(t.rivUsd), index('asset_stats_trending_idx').on(t.trendingScore), index('asset_stats_liquidity_idx').on(t.liquidityScore)],137);138139export const variantStats = pgTable('variant_stats', {140  variantId: text('variant_id').primaryKey(),141  assetId: text('asset_id').notNull(),142  rivUsd: money('riv_usd'),143  rivLowUsd: money('riv_low_usd'),144  rivHighUsd: money('riv_high_usd'),145  rivConfidence: ratio('riv_confidence'),146  rivSampleSize: integer('riv_sample_size').notNull().default(0),147  latestSaleUsd: money('latest_sale_usd'),148  latestSaleAt: ts('latest_sale_at'),149  change30d: ratio('change_30d'),150  change1y: ratio('change_1y'),151  salesCount: integer('sales_count').notNull().default(0),152  sales30d: integer('sales_30d').notNull().default(0),153  activeListings: integer('active_listings').notNull().default(0),154  minAskUsd: money('min_ask_usd'),155  liquidityScore: real('liquidity_score'),156  updatedAt: updatedAt(),157});158159export const images = pgTable(160  'images',161  {162    id: text('id').primaryKey(),163    assetId: text('asset_id'),164    listingId: text('listing_id'),165    saleId: text('sale_id'),166    sourceId: text('source_id'),167    url: text('url').notNull(),168    role: text('role').notNull().default('gallery'), // hero | gallery | source169    width: integer('width'),170    height: integer('height'),171    phash: text('phash'),172    embedding: vector('embedding', { dimensions: 512 }),173    attribution: text('attribution'),174    createdAt: createdAt(),175  },176  (t) => [index('images_asset_idx').on(t.assetId), index('images_phash_idx').on(t.phash), uniqueIndex('images_url_uq').on(t.url)],177);178179/** Text embeddings for hybrid search / entity resolution (§112, §138). */180export const assetEmbeddings = pgTable('asset_embeddings', {181  assetId: text('asset_id').primaryKey(),182  model: text('model').notNull(),183  embedding: vector('embedding', { dimensions: 1536 }).notNull(),184  updatedAt: updatedAt(),185});186187export const populationReports = pgTable(188  'population_reports',189  {190    id: text('id').primaryKey(),191    assetId: text('asset_id').notNull(),192    grader: text('grader').notNull(),193    sourceId: text('source_id').notNull(),194    sourceUrl: text('source_url'),195    reportDate: text('report_date').notNull(),196    total: integer('total').notNull(),197    byGrade: jsonb('by_grade').$type<Record<string, number>>().notNull(),198    createdAt: createdAt(),199  },200  (t) => [uniqueIndex('population_reports_uq').on(t.assetId, t.grader, t.reportDate)],201);202203/** Empirical grade premiums (§118). */204export const gradePremiums = pgTable(205  'grade_premiums',206  {207    id: text('id').primaryKey(),208    categorySlug: text('category_slug').notNull(),209    grader: text('grader').notNull(),210    grade: text('grade').notNull(),211    marketMultiplier: real('market_multiplier').notNull(),212    sampleSize: integer('sample_size').notNull(),213    computedAt: ts('computed_at').notNull(),214  },215  (t) => [uniqueIndex('grade_premiums_uq').on(t.categorySlug, t.grader, t.grade)],216);217
Files · d93392862678cbf30e175697fcb812bc4ff0b5a7
Menu

Theme