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%

Images: self-hosted proxy/cache with per-host rules, image-processing worker, placeholder-aware components; migration 0003 (agent R)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 17 days ago (Sep 7, 2026) parent d6b1739

28 changed files +9,243 −44

modified .gitignore +1 −0
@@ -15,3 +15,4 @@ data/cache/
15 15 data/raw/
16 16 tmp/
17 17 .claude/
18 +data/images/
modified apps/web/next.config.ts +2 −0
@@ -31,6 +31,8 @@ const nextConfig: NextConfig = {
31 31 },
32 32 experimental: {
33 33 optimizePackageImports: ['lucide-react'],
34 + // The image cache core is shared with the worker (workers/image-processing/core.ts) via a relative re-export.
35 + externalDir: true,
34 36 // Workspace packages use NodeNext-style `./file.js` imports that resolve to .ts sources.
35 37 // Turbopack does not rewrite these outside the app root, so the app is built with webpack
36 38 // (`next dev/build --webpack`) where extensionAlias handles the mapping.
modified apps/web/package.json +4 −3
@@ -15,9 +15,11 @@
15 15 "@rareindex/connectors": "workspace:*",
16 16 "@rareindex/database": "workspace:*",
17 17 "@rareindex/notify": "workspace:*",
18 + "@rareindex/search": "workspace:*",
18 19 "@rareindex/shared": "workspace:*",
19 20 "@rareindex/taxonomy": "workspace:*",
20 21 "@tanstack/react-query": "^5.90.0",
22 + "drizzle-orm": "^0.45.0",
21 23 "lucide-react": "^1.0.0",
22 24 "next": "16.3.4",
23 25 "otpauth": "^9.5.2",
@@ -26,9 +28,8 @@
26 28 "react": "19.2.8",
27 29 "react-dom": "19.2.8",
28 30 "server-only": "^0.0.1",
29 − "zod": "^4.0.0",
30 − "@rareindex/search": "workspace:*",
31 − "drizzle-orm": "^0.45.0"
31 + "sharp": "^0.35.0",
32 + "zod": "^4.0.0"
32 33 },
33 34 "devDependencies": {
34 35 "@tailwindcss/postcss": "^4",
added apps/web/src/app/img/[key]/route.ts +97 −0
@@ -0,0 +1,97 @@
1 +import { NextResponse, type NextRequest } from 'next/server';
2 +import { createReadStream } from 'node:fs';
3 +import { stat } from 'node:fs/promises';
4 +import { Readable } from 'node:stream';
5 +import { getSql } from '@rareindex/database';
6 +import { ensureOriginal, ensureVariant, findOriginal, imageKey, nearestWidth, negativeFor, variantPath, type OriginalInfo } from '@/lib/images-core';
7 +import { verifyImageSignature } from '@/lib/images';
8 +
9 +export const dynamic = 'force-dynamic';
10 +export const runtime = 'nodejs';
11 +
12 +const IMMUTABLE = 'public, max-age=31536000, immutable';
13 +const NEGATIVE = 'public, max-age=3600, stale-while-revalidate=600';
14 +
15 +/**
16 + * GET /img/<sha1>.<webp|avif>?w=<width>&u=<base64url(url)>&s=<sig>
17 + * Serves a cached, resized copy of a third-party product image. The original URL must be
18 + * HMAC-signed by the server (any page that renders it) or already known in the `images` table.
19 + */
20 +export async function GET(req: NextRequest, ctx: { params: Promise<{ key: string }> }) {
21 + const { key: rawKey } = await ctx.params;
22 + const m = rawKey.match(/^([a-f0-9]{40})(?:\.(webp|avif))?$/);
23 + if (!m) return new NextResponse('Not found', { status: 404, headers: { 'cache-control': NEGATIVE } });
24 + const key = m[1]!;
25 + const fmt = (m[2] as 'webp' | 'avif' | undefined) ?? 'webp';
26 + const width = nearestWidth(req.nextUrl.searchParams.get('w'));
27 +
28 + // Fast path: variant already on disk.
29 + try {
30 + const vpath = variantPath(key, width, fmt);
31 + const s = await stat(vpath);
32 + return fileResponse(vpath, s.size, `image/${fmt}`, req);
33 + } catch {
34 + /* build below */
35 + }
36 +
37 + // Resolve the source URL: signed param first, then DB lookup by cache key / sha1(url).
38 + let url: string | null = null;
39 + const u = req.nextUrl.searchParams.get('u');
40 + const s = req.nextUrl.searchParams.get('s');
41 + if (u && s) {
42 + try {
43 + const decoded = Buffer.from(u, 'base64url').toString('utf8');
44 + if (imageKey(decoded) === key && verifyImageSignature(decoded, s)) url = decoded;
45 + } catch {
46 + url = null;
47 + }
48 + }
49 + if (!url) {
50 + try {
51 + const sql = getSql();
52 + const rows = (await sql`select url from images where cache_key = ${key} limit 1`) as Array<{ url: string }>;
53 + url = rows[0]?.url ?? null;
54 + } catch {
55 + url = null;
56 + }
57 + }
58 +
59 + let info: OriginalInfo | null = await findOriginal(key);
60 + if (!info) {
61 + if (!url) return new NextResponse('Unknown image', { status: 404, headers: { 'cache-control': NEGATIVE } });
62 + if (negativeFor(url)) return new NextResponse('Image unavailable', { status: 404, headers: { 'cache-control': NEGATIVE, 'x-ri-image': 'negative-cache' } });
63 + const out = await ensureOriginal(url);
64 + if (!out.ok) {
65 + void recordFailure(key, url, out.status, out.reason);
66 + return new NextResponse('Image unavailable', { status: 404, headers: { 'cache-control': NEGATIVE, 'x-ri-image': out.status } });
67 + }
68 + info = out.info;
69 + }
70 + try {
71 + const vpath = await ensureVariant(info, width, fmt);
72 + const st = await stat(vpath);
73 + return fileResponse(vpath, st.size, `image/${fmt}`, req);
74 + } catch (err) {
75 + console.error('[img] variant failed', key, err instanceof Error ? err.message : err);
76 + return new NextResponse('Image processing failed', { status: 500, headers: { 'cache-control': 'no-store' } });
77 + }
78 +}
79 +
80 +function fileResponse(filePath: string, size: number, contentType: string, req: NextRequest): NextResponse {
81 + const etag = `"${size}-${filePath.slice(-24).replace(/[^a-z0-9]/gi, '')}"`;
82 + if (req.headers.get('if-none-match') === etag) return new NextResponse(null, { status: 304, headers: { etag, 'cache-control': IMMUTABLE } });
83 + const stream = Readable.toWeb(createReadStream(filePath)) as unknown as ReadableStream;
84 + return new NextResponse(stream, {
85 + status: 200,
86 + headers: { 'content-type': contentType, 'content-length': String(size), 'cache-control': IMMUTABLE, etag, 'x-content-type-options': 'nosniff', 'accept-ch': 'DPR, Width' },
87 + });
88 +}
89 +
90 +async function recordFailure(key: string, url: string, status: string, reason: string): Promise<void> {
91 + try {
92 + const sql = getSql();
93 + await sql`update images set status = ${status}, error = ${reason.slice(0, 200)}, checked_at = now(), cache_key = ${key} where url = ${url}`;
94 + } catch {
95 + /* best effort */
96 + }
97 +}
modified apps/web/src/components/asset/asset-gallery.tsx +9 −5
@@ -1,6 +1,6 @@
1 1 'use client';
2 2
3 −import Image from 'next/image';
3 +import { SmartImage, ImagePlaceholder, type PlaceholderGlyph } from '@/components/ui/smart-image';
4 4 import { useCallback, useEffect, useRef, useState } from 'react';
5 5 import { ChevronLeft, ChevronRight, Maximize2, X } from 'lucide-react';
6 6 import { cn } from '@/lib/format';
@@ -8,13 +8,17 @@ import { cn } from '@/lib/format';
8 8 export interface GalleryImage {
9 9 url: string;
10 10 caption?: string | null;
11 + /** cached /img sources (added by the server component) */
12 + src?: string | null;
13 + srcSet?: string | null;
14 + full?: string | null;
11 15 }
12 16
13 17 /**
14 18 * Hero image with a full-screen, swipeable gallery (scroll-snap + keyboard). Images are shown
15 19 * `object-contain` on a neutral surface so cards, watches and boxes keep their proportions.
16 20 */
17 −export function AssetGallery({ images, alt, className, priority = true }: { images: GalleryImage[]; alt: string; className?: string; priority?: boolean }) {
21 +export function AssetGallery({ images, alt, className, priority = true, glyph = 'box' }: { images: GalleryImage[]; alt: string; className?: string; priority?: boolean; glyph?: PlaceholderGlyph }) {
18 22 const [open, setOpen] = useState(false);
19 23 const [index, setIndex] = useState(0);
20 24 const track = useRef<HTMLDivElement>(null);
@@ -48,7 +52,7 @@ export function AssetGallery({ images, alt, className, priority = true }: { imag
48 52 if (!hero) {
49 53 return (
50 54 <div className={cn('relative flex items-center justify-center overflow-hidden rounded-lg border border-border bg-inset', className)}>
51 − <span className="text-[10px] font-medium uppercase tracking-wider text-subtle">No image</span>
55 + <ImagePlaceholder label="No image" glyph={glyph} />
52 56 </div>
53 57 );
54 58 }
@@ -56,7 +60,7 @@ export function AssetGallery({ images, alt, className, priority = true }: { imag
56 60 return (
57 61 <>
58 62 <button type="button" onClick={() => setOpen(true)} className={cn('group relative block overflow-hidden rounded-lg border border-border bg-inset text-left', className)} aria-label={`Open image gallery for ${alt}`}>
59 − <Image src={hero.url} alt={alt} fill sizes="(max-width: 768px) 100vw, 360px" className="object-contain p-2 transition-transform duration-300 group-hover:scale-[1.02]" priority={priority} />
63 + <SmartImage src={hero.src ?? null} srcSet={hero.srcSet ?? null} sizes="(max-width: 768px) 100vw, 360px" alt={alt} priority={priority} imgClassName="p-2 transition-transform duration-300 group-hover:scale-[1.02]" placeholder={{ label: 'No image', glyph }} />
60 64 <span className="absolute bottom-2 right-2 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-1 text-[10px] font-medium text-white backdrop-blur-sm">
61 65 <Maximize2 className="h-3 w-3" />
62 66 {images.length > 1 ? `${images.length} photos` : 'View'}
@@ -83,7 +87,7 @@ export function AssetGallery({ images, alt, className, priority = true }: { imag
83 87 >
84 88 {images.map((im, i) => (
85 89 <figure key={`${im.url}-${i}`} className="relative flex h-full w-full shrink-0 snap-center items-center justify-center p-2">
86 − <Image src={im.url} alt={`${alt} — image ${i + 1}`} fill sizes="100vw" className="object-contain" unoptimized />
90 + <SmartImage src={im.full ?? im.src ?? null} alt={`${alt} — image ${i + 1}`} priority={i === index} placeholder={{ glyph }} />
87 91 {im.caption ? <figcaption className="absolute bottom-3 left-3 right-3 truncate text-center text-[11px] text-white/70">{im.caption}</figcaption> : null}
88 92 </figure>
89 93 ))}
modified apps/web/src/components/asset/asset-header.tsx +3 −1
@@ -6,6 +6,8 @@ import { Breadcrumbs } from '@/components/ui/page-header';
6 6 import { Badge, Delta } from '@/components/ui/primitives';
7 7 import { ScoreMeter } from '@/components/ui/evidence';
8 8 import { AssetGallery, type GalleryImage } from './asset-gallery';
9 +import { imageProps } from '@/lib/images';
10 +import { glyphForFamily } from '@/lib/image-glyph';
9 11 import { AssetActions } from './asset-actions';
10 12 import { VariantTable } from './variant-table';
11 13
@@ -45,7 +47,7 @@ export function AssetHeader({ asset, variants, activeVariant, tabHref, guide = n
45 47 <div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_340px] lg:items-start">
46 48 {/* identity */}
47 49 <div className="grid gap-4 sm:grid-cols-[200px_minmax(0,1fr)] md:grid-cols-[240px_minmax(0,1fr)]">
48 − <AssetGallery images={images} alt={asset.title} className="aspect-[4/3] w-full sm:aspect-square" />
50 + <AssetGallery images={images.map((im) => ({ ...im, ...(imageProps(im.url, { maxWidth: 768 }) ?? {}), full: imageProps(im.url, { maxWidth: 1200 })?.src ?? null }))} alt={asset.title} glyph={glyphForFamily(asset.familySlug)} className="aspect-[4/3] w-full sm:aspect-square" />
49 51 <div className="min-w-0">
50 52 <h1 className="text-[22px] font-semibold leading-tight tracking-tight text-fg sm:text-2xl lg:text-[26px]">{asset.title}</h1>
51 53 <div className="mt-2 flex flex-wrap items-center gap-1.5 text-[11px]">
modified apps/web/src/components/asset/asset-tabs.tsx +7 −2
@@ -1,5 +1,7 @@
1 −import Image from 'next/image';
2 1 import Link from 'next/link';
2 +import { imageProps } from '@/lib/images';
3 +import { SmartImage } from '@/components/ui/smart-image';
4 +import { glyphForFamily } from '@/lib/image-glyph';
3 5 import type { AssetDetail, VariantRow } from '@/lib/queries/assets';
4 6 import { getAssetImages, getAssetListings, getAssetObservations, getAssetSalePoints, getAssetSales, getAssetSnapshots, getAssetSources, getComparables, getGradeDistribution, getLatestValuation, getMarketplaceDistribution, getPopulation, getPriceDistribution, getSetSiblings, getSimilarAssets, attachGuidePrices, getValuationHistory } from '@/lib/queries/assets';
5 7 import { AssetCardGrid } from '@/components/market/asset-list';
@@ -331,7 +333,10 @@ export async function ImagesTab({ asset }: { asset: AssetDetail }) {
331 333 {imgs.map((im) => (
332 334 <figure key={im.id} className="card overflow-hidden">
333 335 <div className="relative aspect-square bg-inset">
334 − <Image src={im.url} alt={asset.title} fill sizes="(max-width: 640px) 50vw, 200px" className="object-contain" />
336 + {(() => {
337 + const ip = imageProps(im.url, { maxWidth: 384 });
338 + return <SmartImage src={ip?.src ?? null} srcSet={ip?.srcSet ?? null} sizes="(max-width: 640px) 50vw, 200px" alt={asset.title} placeholder={{ glyph: glyphForFamily(asset.familySlug) }} />;
339 + })()}
335 340 </div>
336 341 <figcaption className="truncate px-2 py-1 text-[10px] text-subtle">
337 342 {im.role}
modified apps/web/src/components/market/asset-tile.tsx +2 −19
@@ -1,4 +1,3 @@
1 −import Image from 'next/image';
2 1 import Link from 'next/link';
3 2 import type { ReactNode } from 'react';
4 3 import type { AssetCard as AssetCardData } from '@/lib/queries/assets';
@@ -8,24 +7,8 @@ import { Delta } from '@/components/ui/primitives';
8 7
9 8 export type TileMetric = 'change7d' | 'change1d' | 'change30d' | 'trending' | 'watchers' | 'riv' | 'opportunity' | 'new' | 'guide' | 'latestSale';
10 9
11 −/** Object image: cards/comics are portrait (4:5), everything else squares; always `contain` on a neutral field. */
12 −function aspectFor(familySlug: string): string {
13 − return familySlug === 'trading_cards' || familySlug === 'sports_cards' || familySlug === 'comics' || familySlug === 'manga' || familySlug === 'books' ? 'aspect-[4/5]' : 'aspect-square';
14 −}
15 −
16 −export function TileImage({ src, alt, familySlug, sizes = '(min-width: 1280px) 200px, (min-width: 768px) 25vw, 45vw', priority = false, className }: { src: string | null | undefined; alt: string; familySlug: string; sizes?: string; priority?: boolean; className?: string }) {
17 − return (
18 − <span className={cn('relative block w-full overflow-hidden rounded-[5px] bg-sunken', aspectFor(familySlug), className)}>
19 − {src ? (
20 − <Image src={src} alt={alt} fill sizes={sizes} priority={priority} loading={priority ? undefined : 'lazy'} className="object-contain p-2 transition-transform duration-300 ease-out group-hover:scale-[1.03]" unoptimized={src.endsWith('.svg')} />
21 − ) : (
22 − <span className="absolute inset-0 flex items-center justify-center">
23 − <span className="t-label">No image</span>
24 − </span>
25 − )}
26 − </span>
27 − );
28 −}
10 +import { TileImage } from '@/components/ui/tile-image';
11 +export { TileImage };
29 12
30 13 /** Price line: RIV with confidence, or the guide price honestly labelled, or the latest sale. */
31 14 export function PriceLine({ a, size = 'md', className }: { a: AssetCardData; size?: 'sm' | 'md' | 'lg'; className?: string }) {
modified apps/web/src/components/market/bits.tsx +2 −8
@@ -1,4 +1,3 @@
1 −import Image from 'next/image';
2 1 import Link from 'next/link';
3 2 import { ExternalLink } from 'lucide-react';
4 3 import { cn, fmtMoney } from '@/lib/format';
@@ -7,13 +6,8 @@ import { Badge } from '@/components/ui/primitives';
7 6
8 7 /** Small shared building blocks for market tables and cards. */
9 8
10 −export function Thumb({ src, alt, size = 40, className, rounded = 'rounded-sm' }: { src: string | null | undefined; alt: string; size?: number; className?: string; rounded?: string }) {
11 − return (
12 − <span className={cn('relative inline-block shrink-0 overflow-hidden bg-inset', rounded, className)} style={{ width: size, height: size }}>
13 − {src ? <Image src={src} alt={alt} fill sizes={`${size}px`} className="object-cover" unoptimized={src.endsWith('.svg')} /> : <span className="absolute inset-0 flex items-center justify-center text-[9px] uppercase tracking-wider text-subtle">n/a</span>}
14 − </span>
15 − );
16 −}
9 +import { Thumb } from '@/components/ui/tile-image';
10 +export { Thumb };
17 11
18 12 export function CategoryTag({ slug, className }: { slug: string | null | undefined; className?: string }) {
19 13 if (!slug) return null;
modified apps/web/src/components/market/category-grid.tsx +7 −2
@@ -1,4 +1,6 @@
1 −import Image from 'next/image';
1 +import { imageProps } from '@/lib/images';
2 +import { SmartImage } from '@/components/ui/smart-image';
3 +import { glyphForFamily } from '@/lib/image-glyph';
2 4 import Link from 'next/link';
3 5 import type { MarketRow } from '@/lib/queries/markets';
4 6 import { fmtNum, cn } from '@/lib/format';
@@ -19,7 +21,10 @@ export function CategoryGrid({ rows, thumbs, className, limit, sparklines, mobil
19 21 return (
20 22 <Link key={node.slug} href={`/markets/${node.slug}`} className={cn('card card-hover group flex flex-col overflow-hidden', empty && 'opacity-60', hiddenOnMobile && 'hidden sm:flex')}>
21 23 <span className="relative block aspect-[5/3] w-full overflow-hidden bg-sunken">
22 − {src ? <Image src={src} alt="" fill sizes="(min-width: 1280px) 220px, (min-width: 640px) 33vw, 50vw" className="object-contain p-3 transition-transform duration-300 ease-out group-hover:scale-[1.04]" unoptimized={src.endsWith('.svg')} /> : <span className="absolute inset-0 flex items-center justify-center"><span className="t-label">{empty ? 'No data yet' : node.short ?? node.name}</span></span>}
24 + {(() => {
25 + const ip = imageProps(src, { maxWidth: 384 });
26 + return <SmartImage src={ip?.src ?? null} srcSet={ip?.srcSet ?? null} sizes="(min-width: 1280px) 220px, (min-width: 640px) 33vw, 50vw" alt="" imgClassName="p-3 transition-transform duration-300 ease-out group-hover:scale-[1.04]" placeholder={{ label: empty ? 'No data yet' : node.short ?? node.name, glyph: glyphForFamily(node.familySlug) }} />;
27 + })()}
23 28 {spark && spark.length > 2 ? (
24 29 <span className="absolute bottom-1.5 right-1.5 rounded-sm bg-elevated/80 px-1 backdrop-blur-sm">
25 30 <Sparkline values={spark} width={64} height={18} />
added apps/web/src/components/ui/smart-image.tsx +155 −0
@@ -0,0 +1,155 @@
1 +'use client';
2 +
3 +import { useState, type CSSProperties } from 'react';
4 +import { cn } from '@/lib/format';
5 +import type { PlaceholderGlyph } from '@/lib/image-glyph';
6 +export { glyphForFamily, type PlaceholderGlyph } from '@/lib/image-glyph';
7 +
8 +/**
9 + * Plain, lazy `<img>` bound to the /img cache (src + srcSet computed on the server) with a
10 + * designed placeholder — never the browser's broken-image glyph. Falls back on load error.
11 + */
12 +export interface SmartImageProps {
13 + src: string | null;
14 + srcSet?: string | null;
15 + sizes?: string;
16 + alt: string;
17 + fit?: 'contain' | 'cover';
18 + priority?: boolean;
19 + className?: string;
20 + imgClassName?: string;
21 + style?: CSSProperties;
22 + placeholder?: { label?: string | null; glyph?: PlaceholderGlyph };
23 + /** small thumbnail: glyph only */
24 + compact?: boolean;
25 +}
26 +
27 +
28 +function Glyph({ kind, className }: { kind: PlaceholderGlyph; className?: string }) {
29 + const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
30 + switch (kind) {
31 + case 'card':
32 + return (
33 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
34 + <rect x="5" y="3" width="14" height="18" rx="1.5" />
35 + <rect x="7.5" y="5.5" width="9" height="7" rx="0.5" />
36 + <path d="M7.5 15.5h9M7.5 18h6" />
37 + </svg>
38 + );
39 + case 'comic':
40 + return (
41 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
42 + <rect x="4.5" y="3" width="15" height="18" rx="1" />
43 + <path d="M4.5 8h15M9 3v18M4.5 15h15" />
44 + </svg>
45 + );
46 + case 'watch':
47 + return (
48 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
49 + <circle cx="12" cy="12" r="6" />
50 + <path d="M12 9v3l2 1.5M9.5 6l.7-3h3.6l.7 3M9.5 18l.7 3h3.6l.7-3" />
51 + </svg>
52 + );
53 + case 'brick':
54 + return (
55 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
56 + <path d="M3 9h18v10H3zM6 9V6.5h4V9M14 9V6.5h4V9" />
57 + </svg>
58 + );
59 + case 'game':
60 + return (
61 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
62 + <rect x="6" y="3" width="12" height="18" rx="1.5" />
63 + <rect x="8.5" y="6" width="7" height="6" rx="0.5" />
64 + <path d="M9 16h6" />
65 + </svg>
66 + );
67 + case 'sneaker':
68 + return (
69 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
70 + <path d="M3 15c3 0 5-1 7-4l2 1.5c2 1.5 5 1.5 9 3.5v2H3z" />
71 + <path d="M3 18h18" />
72 + </svg>
73 + );
74 + case 'coin':
75 + return (
76 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
77 + <circle cx="12" cy="12" r="8" />
78 + <circle cx="12" cy="12" r="4.5" />
79 + </svg>
80 + );
81 + case 'bottle':
82 + return (
83 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
84 + <path d="M10 3h4v4l2 3v10a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V10l2-3z" />
85 + <path d="M8 14h8" />
86 + </svg>
87 + );
88 + case 'car':
89 + return (
90 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
91 + <path d="M4 14l2-5h12l2 5v4H4z" />
92 + <circle cx="7.5" cy="17.5" r="1.5" />
93 + <circle cx="16.5" cy="17.5" r="1.5" />
94 + </svg>
95 + );
96 + case 'toy':
97 + return (
98 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
99 + <circle cx="12" cy="8" r="4" />
100 + <path d="M6 21v-4a6 6 0 0 1 12 0v4" />
101 + </svg>
102 + );
103 + case 'book':
104 + return (
105 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
106 + <path d="M5 4h6a2 2 0 0 1 2 2v14a2 2 0 0 0-2-2H5zM19 4h-6a2 2 0 0 0-2 2v14a2 2 0 0 1 2-2h6z" />
107 + </svg>
108 + );
109 + case 'art':
110 + return (
111 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
112 + <rect x="3.5" y="4.5" width="17" height="15" rx="1" />
113 + <path d="M6 17l4-5 3 3 2-2 3 4" />
114 + <circle cx="15.5" cy="9" r="1.2" />
115 + </svg>
116 + );
117 + default:
118 + return (
119 + <svg viewBox="0 0 24 24" className={className} aria-hidden {...p}>
120 + <path d="M4 8l8-4 8 4v8l-8 4-8-4zM4 8l8 4 8-4M12 12v8" />
121 + </svg>
122 + );
123 + }
124 +}
125 +
126 +export function ImagePlaceholder({ label, glyph = 'box', compact = false, className }: { label?: string | null; glyph?: PlaceholderGlyph; compact?: boolean; className?: string }) {
127 + return (
128 + <span className={cn('absolute inset-0 flex flex-col items-center justify-center gap-1 bg-inset text-subtle', className)} role="img" aria-label={label ? `${label} (no image)` : 'No image'}>
129 + <Glyph kind={glyph} className={compact ? 'h-1/2 w-1/2 max-h-5 max-w-5' : 'h-8 w-8 opacity-70'} />
130 + {!compact && label ? <span className="t-caption max-w-[80%] truncate text-center">{label}</span> : null}
131 + </span>
132 + );
133 +}
134 +
135 +export function SmartImage({ src, srcSet, sizes, alt, fit = 'contain', priority = false, className, imgClassName, style, placeholder, compact = false }: SmartImageProps) {
136 + const [failed, setFailed] = useState(false);
137 + if (!src || failed) return <ImagePlaceholder label={placeholder?.label ?? null} glyph={placeholder?.glyph ?? 'box'} compact={compact} className={className} />;
138 + return (
139 + // eslint-disable-next-line @next/next/no-img-element
140 + <img
141 + src={src}
142 + srcSet={srcSet ?? undefined}
143 + sizes={srcSet ? sizes ?? '100vw' : undefined}
144 + alt={alt}
145 + loading={priority ? 'eager' : 'lazy'}
146 + fetchPriority={priority ? 'high' : 'auto'}
147 + decoding="async"
148 + referrerPolicy="no-referrer"
149 + draggable={false}
150 + onError={() => setFailed(true)}
151 + style={style}
152 + className={cn('absolute inset-0 h-full w-full', fit === 'cover' ? 'object-cover' : 'object-contain', imgClassName)}
153 + />
154 + );
155 +}
added apps/web/src/components/ui/tile-image.tsx +64 −0
@@ -0,0 +1,64 @@
1 +import { imageProps } from '@/lib/images';
2 +import { cn } from '@/lib/format';
3 +import { SmartImage } from './smart-image';
4 +import { glyphForFamily } from '@/lib/image-glyph';
5 +
6 +/**
7 + * Server component: turns a third-party URL into cached /img sources and renders the client
8 + * <SmartImage> with an explicit aspect ratio (no layout shift) and a category placeholder.
9 + */
10 +export function aspectForFamily(familySlug: string | null | undefined): string {
11 + return familySlug === 'trading_cards' || familySlug === 'sports_cards' || familySlug === 'comics' || familySlug === 'manga' || familySlug === 'books' ? 'aspect-[4/5]' : 'aspect-square';
12 +}
13 +
14 +export function TileImage({
15 + src,
16 + alt,
17 + familySlug,
18 + label,
19 + sizes = '(min-width: 1280px) 200px, (min-width: 768px) 25vw, 45vw',
20 + maxWidth = 384,
21 + priority = false,
22 + fit = 'contain',
23 + padded = true,
24 + className,
25 + imgClassName,
26 +}: {
27 + src: string | null | undefined;
28 + alt: string;
29 + familySlug: string | null | undefined;
30 + label?: string | null;
31 + sizes?: string;
32 + maxWidth?: number;
33 + priority?: boolean;
34 + fit?: 'contain' | 'cover';
35 + padded?: boolean;
36 + className?: string;
37 + imgClassName?: string;
38 +}) {
39 + const props = imageProps(src, { maxWidth });
40 + return (
41 + <span className={cn('relative block w-full overflow-hidden rounded-[5px] bg-sunken', aspectForFamily(familySlug), className)}>
42 + <SmartImage
43 + src={props?.src ?? null}
44 + srcSet={props?.srcSet ?? null}
45 + sizes={sizes}
46 + alt={alt}
47 + fit={fit}
48 + priority={priority}
49 + imgClassName={cn(padded ? 'p-2' : '', 'transition-transform duration-300 ease-out group-hover:scale-[1.03]', imgClassName)}
50 + placeholder={{ label: label ?? null, glyph: glyphForFamily(familySlug) }}
51 + />
52 + </span>
53 + );
54 +}
55 +
56 +/** Square thumbnail for tables and dense rows. */
57 +export function Thumb({ src, alt, size = 40, familySlug, className, rounded = 'rounded-sm', fit = 'cover' }: { src: string | null | undefined; alt: string; size?: number; familySlug?: string | null; className?: string; rounded?: string; fit?: 'contain' | 'cover' }) {
58 + const props = imageProps(src, { maxWidth: Math.max(96, size * 2) });
59 + return (
60 + <span className={cn('relative inline-block shrink-0 overflow-hidden bg-inset', rounded, className)} style={{ width: size, height: size }}>
61 + <SmartImage src={props?.src ?? null} srcSet={props?.srcSet ?? null} sizes={`${size}px`} alt={alt} fit={fit} compact placeholder={{ glyph: glyphForFamily(familySlug) }} />
62 + </span>
63 + );
64 +}
added apps/web/src/lib/image-glyph.ts +59 −0
@@ -0,0 +1,59 @@
1 +/** Placeholder glyph per taxonomy family — plain module so both server and client components can call it. */
2 +export type PlaceholderGlyph = 'card' | 'comic' | 'watch' | 'brick' | 'game' | 'sneaker' | 'coin' | 'bottle' | 'car' | 'toy' | 'book' | 'art' | 'box';
3 +
4 +export function glyphForFamily(familySlug: string | null | undefined): PlaceholderGlyph {
5 + switch (familySlug) {
6 + case 'trading_cards':
7 + case 'sports_cards':
8 + return 'card';
9 + case 'comics':
10 + case 'manga':
11 + return 'comic';
12 + case 'watches':
13 + return 'watch';
14 + case 'lego':
15 + return 'brick';
16 + case 'video_games':
17 + case 'gaming_hardware':
18 + case 'arcade_pinball':
19 + case 'board_games':
20 + return 'game';
21 + case 'sneakers':
22 + case 'fashion_streetwear':
23 + return 'sneaker';
24 + case 'coins':
25 + case 'banknotes':
26 + case 'medals':
27 + case 'stamps':
28 + return 'coin';
29 + case 'wine':
30 + case 'whisky':
31 + case 'rum':
32 + case 'cognac':
33 + case 'perfume':
34 + return 'bottle';
35 + case 'automobiles':
36 + case 'motorcycles':
37 + case 'model_cars':
38 + return 'car';
39 + case 'funko':
40 + case 'designer_toys':
41 + case 'action_figures':
42 + case 'vintage_toys':
43 + case 'dolls':
44 + case 'plush':
45 + return 'toy';
46 + case 'books':
47 + case 'historical_documents':
48 + case 'maps':
49 + return 'book';
50 + case 'art':
51 + case 'contemporary_art':
52 + case 'photography':
53 + case 'movie_posters':
54 + case 'animation_art':
55 + return 'art';
56 + default:
57 + return 'box';
58 + }
59 +}
added apps/web/src/lib/images-core.test.ts +35 −0
@@ -0,0 +1,35 @@
1 +import { describe, expect, it } from 'vitest';
2 +import { dhashFromGray, hamming, hostRule, imageKey, nearestWidth, requestHeadersFor } from './images-core';
3 +
4 +describe('images-core', () => {
5 + it('computes a stable dHash and hamming distance', () => {
6 + const grad = Array.from({ length: 72 }, (_, i) => (i % 9) * 30); // left→right ramp: all bits 0
7 + const h1 = dhashFromGray(grad);
8 + expect(h1).toBe('0000000000000000');
9 + const inv = Array.from({ length: 72 }, (_, i) => 255 - (i % 9) * 30);
10 + const h2 = dhashFromGray(inv);
11 + expect(h2).toBe('ffffffffffffffff');
12 + expect(hamming(h1, h2)).toBe(64);
13 + expect(hamming(h1, h1)).toBe(0);
14 + });
15 +
16 + it('applies host rules', () => {
17 + expect(hostRule('cards.scryfall.io').maxConcurrent).toBe(4);
18 + const h = requestHeadersFor(new URL('https://storage.googleapis.com/images.pricecharting.com/x/240.jpg'));
19 + expect(h.referer).toBe('https://www.pricecharting.com/');
20 + expect(h['user-agent']).toMatch(/RareIndexImageCache/);
21 + expect(h['user-agent']).not.toBe('node');
22 + const c = requestHeadersFor(new URL('https://www.comicconnect.com/coverimages/x.jpg'));
23 + expect(c.referer).toBe('https://www.comicconnect.com/');
24 + expect(requestHeadersFor(new URL('https://images.ygoprodeck.com/images/cards/1.jpg')).referer).toBeUndefined();
25 + });
26 +
27 + it('snaps widths and keys urls', () => {
28 + expect(nearestWidth(100)).toBe(192);
29 + expect(nearestWidth(384)).toBe(384);
30 + expect(nearestWidth(5000)).toBe(1200);
31 + expect(nearestWidth('abc')).toBe(384);
32 + expect(imageKey('https://a/b.jpg')).toHaveLength(40);
33 + expect(imageKey('https://a/b.jpg')).toBe(imageKey('https://a/b.jpg'));
34 + });
35 +});
added apps/web/src/lib/images-core.ts +2 −0
@@ -0,0 +1,2 @@
1 +/** Re-export of the shared image cache core (ESM source lives in workers/image-processing/core.ts). */
2 +export * from '../../../../workers/image-processing/core';
added apps/web/src/lib/images.ts +46 −0
@@ -0,0 +1,46 @@
1 +import 'server-only';
2 +import { createHmac } from 'node:crypto';
3 +import { IMAGE_WIDTHS, imageKey, type ImageWidth } from './images-core';
4 +
5 +/**
6 + * Server-side helpers that turn a third-party image URL into signed `/img/…` proxy URLs.
7 + * Signing (HMAC over the URL with SESSION_SECRET) means only URLs the server chose to render
8 + * can be proxied — never arbitrary hosts.
9 + */
10 +function secret(): string {
11 + return process.env.SESSION_SECRET ?? 'dev-only-session-secret-change-me-please';
12 +}
13 +
14 +export function signImageUrl(url: string): string {
15 + return createHmac('sha256', secret()).update(`img:${url}`).digest('base64url').slice(0, 20);
16 +}
17 +
18 +export function verifyImageSignature(url: string, sig: string): boolean {
19 + return sig.length >= 20 && signImageUrl(url) === sig;
20 +}
21 +
22 +function b64url(s: string): string {
23 + return Buffer.from(s, 'utf8').toString('base64url');
24 +}
25 +
26 +export function imgSrc(url: string, width: ImageWidth | number = 384, fmt: 'webp' | 'avif' = 'webp'): string {
27 + const key = imageKey(url);
28 + return `/img/${key}.${fmt}?w=${width}&u=${b64url(url)}&s=${signImageUrl(url)}`;
29 +}
30 +
31 +export interface ImageProps {
32 + src: string;
33 + srcSet: string;
34 +}
35 +
36 +/** `src` + `srcSet` for a responsive `<img>`; `maxWidth` trims useless large variants for thumbnails. */
37 +export function imageProps(url: string | null | undefined, opts: { maxWidth?: number } = {}): ImageProps | null {
38 + if (!url || !/^https?:\/\//i.test(url)) return null;
39 + const max = opts.maxWidth ?? 1200;
40 + const widths = IMAGE_WIDTHS.filter((w) => w <= Math.max(96, max * 2));
41 + const list = widths.length ? widths : [IMAGE_WIDTHS[0]];
42 + return {
43 + src: imgSrc(url, list[Math.min(list.length - 1, Math.max(0, list.findIndex((w) => w >= max)))] ?? 384),
44 + srcSet: list.map((w) => `${imgSrc(url, w)} ${w}w`).join(', '),
45 + };
46 +}
added docs/PENDING-SCHEMA-images.md +18 −0
@@ -0,0 +1,18 @@
1 +# Pending schema changes — images pipeline (agent R, 2026-09-07)
2 +
3 +Applied locally with `drizzle-kit push`; the parent generates the migration.
4 +
5 +Table `images` — new columns and indexes:
6 +
7 +| column | type | note |
8 +|---|---|---|
9 +| `status` | text not null default `'unchecked'` | `unchecked` · `ok` · `dead` (404/410/not an image) · `blocked` (400/401/403/429) · `error` (network/timeout, retried after 6 h) |
10 +| `checked_at` | timestamptz | last validation |
11 +| `bytes` | integer | original size |
12 +| `content_type` | text | original MIME |
13 +| `cache_key` | text | `sha1(url)`; key of `RI_DATA_DIR/images/{orig,w96,w192,w384,w768,w1200}/<k[0:2]>/<k>.*` |
14 +| `error` | text | last failure reason (≤200 chars) |
15 +
16 +Indexes: `images_cache_key_idx (cache_key)`, `images_status_idx (status, checked_at)`.
17 +
18 +No other tables touched. `workers/lib/queue.ts` gained the job name `images.process`.
added packages/database/migrations/0003_curved_sage.sql +8 −0
@@ -0,0 +1,8 @@
1 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "status" text DEFAULT 'unchecked' NOT NULL;--> statement-breakpoint
2 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "checked_at" timestamp with time zone;--> statement-breakpoint
3 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "bytes" integer;--> statement-breakpoint
4 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "content_type" text;--> statement-breakpoint
5 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "cache_key" text;--> statement-breakpoint
6 +ALTER TABLE "images" ADD COLUMN IF NOT EXISTS "error" text;--> statement-breakpoint
7 +CREATE INDEX IF NOT EXISTS "images_cache_key_idx" ON "images" USING btree ("cache_key");--> statement-breakpoint
8 +CREATE INDEX IF NOT EXISTS "images_status_idx" ON "images" USING btree ("status","checked_at");
\ No newline at end of file
added packages/database/migrations/meta/0003_snapshot.json +8203 −0
@@ -0,0 +1,8203 @@
1 +{
2 + "id": "f93403b6-8c53-4b25-bd79-ef5db79c5394",
3 + "prevId": "7a8f1170-2f80-48ba-a038-5a897c56d875",
4 + "version": "7",
5 + "dialect": "postgresql",
6 + "tables": {
7 + "public.brands": {
8 + "name": "brands",
9 + "schema": "",
10 + "columns": {
11 + "slug": {
12 + "name": "slug",
13 + "type": "text",
14 + "primaryKey": true,
15 + "notNull": true
16 + },
17 + "name": {
18 + "name": "name",
19 + "type": "text",
20 + "primaryKey": false,
21 + "notNull": true
22 + },
23 + "category_slugs": {
24 + "name": "category_slugs",
25 + "type": "text[]",
26 + "primaryKey": false,
27 + "notNull": true,
28 + "default": "'{}'::text[]"
29 + },
30 + "country": {
31 + "name": "country",
32 + "type": "text",
33 + "primaryKey": false,
34 + "notNull": false
35 + },
36 + "founded_year": {
37 + "name": "founded_year",
38 + "type": "integer",
39 + "primaryKey": false,
40 + "notNull": false
41 + },
42 + "metadata": {
43 + "name": "metadata",
44 + "type": "jsonb",
45 + "primaryKey": false,
46 + "notNull": true,
47 + "default": "'{}'::jsonb"
48 + },
49 + "created_at": {
50 + "name": "created_at",
51 + "type": "timestamp with time zone",
52 + "primaryKey": false,
53 + "notNull": true,
54 + "default": "now()"
55 + }
56 + },
57 + "indexes": {
58 + "brands_name_idx": {
59 + "name": "brands_name_idx",
60 + "columns": [
61 + {
62 + "expression": "name",
63 + "isExpression": false,
64 + "asc": true,
65 + "nulls": "last"
66 + }
67 + ],
68 + "isUnique": false,
69 + "concurrently": false,
70 + "method": "btree",
71 + "with": {}
72 + }
73 + },
74 + "foreignKeys": {},
75 + "compositePrimaryKeys": {},
76 + "uniqueConstraints": {},
77 + "policies": {},
78 + "checkConstraints": {},
79 + "isRLSEnabled": false
80 + },
81 + "public.categories": {
82 + "name": "categories",
83 + "schema": "",
84 + "columns": {
85 + "slug": {
86 + "name": "slug",
87 + "type": "text",
88 + "primaryKey": true,
89 + "notNull": true
90 + },
91 + "parent_slug": {
92 + "name": "parent_slug",
93 + "type": "text",
94 + "primaryKey": false,
95 + "notNull": false
96 + },
97 + "family_slug": {
98 + "name": "family_slug",
99 + "type": "text",
100 + "primaryKey": false,
101 + "notNull": true
102 + },
103 + "name": {
104 + "name": "name",
105 + "type": "text",
106 + "primaryKey": false,
107 + "notNull": true
108 + },
109 + "short_name": {
110 + "name": "short_name",
111 + "type": "text",
112 + "primaryKey": false,
113 + "notNull": false
114 + },
115 + "description": {
116 + "name": "description",
117 + "type": "text",
118 + "primaryKey": false,
119 + "notNull": false
120 + },
121 + "level": {
122 + "name": "level",
123 + "type": "integer",
124 + "primaryKey": false,
125 + "notNull": true,
126 + "default": 0
127 + },
128 + "phase": {
129 + "name": "phase",
130 + "type": "integer",
131 + "primaryKey": false,
132 + "notNull": true,
133 + "default": 3
134 + },
135 + "active": {
136 + "name": "active",
137 + "type": "boolean",
138 + "primaryKey": false,
139 + "notNull": true,
140 + "default": true
141 + },
142 + "condition_scale": {
143 + "name": "condition_scale",
144 + "type": "text",
145 + "primaryKey": false,
146 + "notNull": false
147 + },
148 + "graders": {
149 + "name": "graders",
150 + "type": "text[]",
151 + "primaryKey": false,
152 + "notNull": true,
153 + "default": "'{}'::text[]"
154 + },
155 + "index_ticker": {
156 + "name": "index_ticker",
157 + "type": "text",
158 + "primaryKey": false,
159 + "notNull": false
160 + },
161 + "attribute_schema": {
162 + "name": "attribute_schema",
163 + "type": "jsonb",
164 + "primaryKey": false,
165 + "notNull": true,
166 + "default": "'{}'::jsonb"
167 + },
168 + "sort_order": {
169 + "name": "sort_order",
170 + "type": "integer",
171 + "primaryKey": false,
172 + "notNull": true,
173 + "default": 0
174 + },
175 + "compliance_flags": {
176 + "name": "compliance_flags",
177 + "type": "text[]",
178 + "primaryKey": false,
179 + "notNull": true,
180 + "default": "'{}'::text[]"
181 + },
182 + "icon": {
183 + "name": "icon",
184 + "type": "text",
185 + "primaryKey": false,
186 + "notNull": false
187 + },
188 + "created_at": {
189 + "name": "created_at",
190 + "type": "timestamp with time zone",
191 + "primaryKey": false,
192 + "notNull": true,
193 + "default": "now()"
194 + },
195 + "updated_at": {
196 + "name": "updated_at",
197 + "type": "timestamp with time zone",
198 + "primaryKey": false,
199 + "notNull": true,
200 + "default": "now()"
201 + }
202 + },
203 + "indexes": {
204 + "categories_parent_idx": {
205 + "name": "categories_parent_idx",
206 + "columns": [
207 + {
208 + "expression": "parent_slug",
209 + "isExpression": false,
210 + "asc": true,
211 + "nulls": "last"
212 + }
213 + ],
214 + "isUnique": false,
215 + "concurrently": false,
216 + "method": "btree",
217 + "with": {}
218 + },
219 + "categories_family_idx": {
220 + "name": "categories_family_idx",
221 + "columns": [
222 + {
223 + "expression": "family_slug",
224 + "isExpression": false,
225 + "asc": true,
226 + "nulls": "last"
227 + }
228 + ],
229 + "isUnique": false,
230 + "concurrently": false,
231 + "method": "btree",
232 + "with": {}
233 + }
234 + },
235 + "foreignKeys": {},
236 + "compositePrimaryKeys": {},
237 + "uniqueConstraints": {},
238 + "policies": {},
239 + "checkConstraints": {},
240 + "isRLSEnabled": false
241 + },
242 + "public.franchises": {
243 + "name": "franchises",
244 + "schema": "",
245 + "columns": {
246 + "slug": {
247 + "name": "slug",
248 + "type": "text",
249 + "primaryKey": true,
250 + "notNull": true
251 + },
252 + "name": {
253 + "name": "name",
254 + "type": "text",
255 + "primaryKey": false,
256 + "notNull": true
257 + },
258 + "owner": {
259 + "name": "owner",
260 + "type": "text",
261 + "primaryKey": false,
262 + "notNull": false
263 + },
264 + "category_slugs": {
265 + "name": "category_slugs",
266 + "type": "text[]",
267 + "primaryKey": false,
268 + "notNull": true,
269 + "default": "'{}'::text[]"
270 + },
271 + "metadata": {
272 + "name": "metadata",
273 + "type": "jsonb",
274 + "primaryKey": false,
275 + "notNull": true,
276 + "default": "'{}'::jsonb"
277 + },
278 + "created_at": {
279 + "name": "created_at",
280 + "type": "timestamp with time zone",
281 + "primaryKey": false,
282 + "notNull": true,
283 + "default": "now()"
284 + }
285 + },
286 + "indexes": {},
287 + "foreignKeys": {},
288 + "compositePrimaryKeys": {},
289 + "uniqueConstraints": {},
290 + "policies": {},
291 + "checkConstraints": {},
292 + "isRLSEnabled": false
293 + },
294 + "public.graders": {
295 + "name": "graders",
296 + "schema": "",
297 + "columns": {
298 + "slug": {
299 + "name": "slug",
300 + "type": "text",
301 + "primaryKey": true,
302 + "notNull": true
303 + },
304 + "name": {
305 + "name": "name",
306 + "type": "text",
307 + "primaryKey": false,
308 + "notNull": true
309 + },
310 + "category_slugs": {
311 + "name": "category_slugs",
312 + "type": "text[]",
313 + "primaryKey": false,
314 + "notNull": true,
315 + "default": "'{}'::text[]"
316 + },
317 + "scale": {
318 + "name": "scale",
319 + "type": "jsonb",
320 + "primaryKey": false,
321 + "notNull": true,
322 + "default": "'{}'::jsonb"
323 + },
324 + "population_url": {
325 + "name": "population_url",
326 + "type": "text",
327 + "primaryKey": false,
328 + "notNull": false
329 + },
330 + "verify_url": {
331 + "name": "verify_url",
332 + "type": "text",
333 + "primaryKey": false,
334 + "notNull": false
335 + },
336 + "active": {
337 + "name": "active",
338 + "type": "boolean",
339 + "primaryKey": false,
340 + "notNull": true,
341 + "default": true
342 + }
343 + },
344 + "indexes": {},
345 + "foreignKeys": {},
346 + "compositePrimaryKeys": {},
347 + "uniqueConstraints": {},
348 + "policies": {},
349 + "checkConstraints": {},
350 + "isRLSEnabled": false
351 + },
352 + "public.sets": {
353 + "name": "sets",
354 + "schema": "",
355 + "columns": {
356 + "slug": {
357 + "name": "slug",
358 + "type": "text",
359 + "primaryKey": true,
360 + "notNull": true
361 + },
362 + "name": {
363 + "name": "name",
364 + "type": "text",
365 + "primaryKey": false,
366 + "notNull": true
367 + },
368 + "code": {
369 + "name": "code",
370 + "type": "text",
371 + "primaryKey": false,
372 + "notNull": false
373 + },
374 + "category_slug": {
375 + "name": "category_slug",
376 + "type": "text",
377 + "primaryKey": false,
378 + "notNull": true
379 + },
380 + "franchise_slug": {
381 + "name": "franchise_slug",
382 + "type": "text",
383 + "primaryKey": false,
384 + "notNull": false
385 + },
386 + "brand_slug": {
387 + "name": "brand_slug",
388 + "type": "text",
389 + "primaryKey": false,
390 + "notNull": false
391 + },
392 + "release_year": {
393 + "name": "release_year",
394 + "type": "integer",
395 + "primaryKey": false,
396 + "notNull": false
397 + },
398 + "release_date": {
399 + "name": "release_date",
400 + "type": "text",
401 + "primaryKey": false,
402 + "notNull": false
403 + },
404 + "language": {
405 + "name": "language",
406 + "type": "text",
407 + "primaryKey": false,
408 + "notNull": false
409 + },
410 + "total_items": {
411 + "name": "total_items",
412 + "type": "integer",
413 + "primaryKey": false,
414 + "notNull": false
415 + },
416 + "identifiers": {
417 + "name": "identifiers",
418 + "type": "jsonb",
419 + "primaryKey": false,
420 + "notNull": true,
421 + "default": "'{}'::jsonb"
422 + },
423 + "metadata": {
424 + "name": "metadata",
425 + "type": "jsonb",
426 + "primaryKey": false,
427 + "notNull": true,
428 + "default": "'{}'::jsonb"
429 + },
430 + "created_at": {
431 + "name": "created_at",
432 + "type": "timestamp with time zone",
433 + "primaryKey": false,
434 + "notNull": true,
435 + "default": "now()"
436 + }
437 + },
438 + "indexes": {
439 + "sets_category_idx": {
440 + "name": "sets_category_idx",
441 + "columns": [
442 + {
443 + "expression": "category_slug",
444 + "isExpression": false,
445 + "asc": true,
446 + "nulls": "last"
447 + }
448 + ],
449 + "isUnique": false,
450 + "concurrently": false,
451 + "method": "btree",
452 + "with": {}
453 + },
454 + "sets_category_code_uq": {
455 + "name": "sets_category_code_uq",
456 + "columns": [
457 + {
458 + "expression": "category_slug",
459 + "isExpression": false,
460 + "asc": true,
461 + "nulls": "last"
462 + },
463 + {
464 + "expression": "code",
465 + "isExpression": false,
466 + "asc": true,
467 + "nulls": "last"
468 + }
469 + ],
470 + "isUnique": true,
471 + "concurrently": false,
472 + "method": "btree",
473 + "with": {}
474 + }
475 + },
476 + "foreignKeys": {},
477 + "compositePrimaryKeys": {},
478 + "uniqueConstraints": {},
479 + "policies": {},
480 + "checkConstraints": {},
481 + "isRLSEnabled": false
482 + },
483 + "public.taxonomy_proposals": {
484 + "name": "taxonomy_proposals",
485 + "schema": "",
486 + "columns": {
487 + "id": {
488 + "name": "id",
489 + "type": "text",
490 + "primaryKey": true,
491 + "notNull": true
492 + },
493 + "proposed_slug": {
494 + "name": "proposed_slug",
495 + "type": "text",
496 + "primaryKey": false,
497 + "notNull": true
498 + },
499 + "name": {
500 + "name": "name",
501 + "type": "text",
502 + "primaryKey": false,
503 + "notNull": true
504 + },
505 + "parent_slug": {
506 + "name": "parent_slug",
507 + "type": "text",
508 + "primaryKey": false,
509 + "notNull": false
510 + },
511 + "evidence": {
512 + "name": "evidence",
513 + "type": "jsonb",
514 + "primaryKey": false,
515 + "notNull": true,
516 + "default": "'{}'::jsonb"
517 + },
518 + "volume_estimate": {
519 + "name": "volume_estimate",
520 + "type": "integer",
521 + "primaryKey": false,
522 + "notNull": false
523 + },
524 + "status": {
525 + "name": "status",
526 + "type": "text",
527 + "primaryKey": false,
528 + "notNull": true,
529 + "default": "'pending'"
530 + },
531 + "decided_by": {
532 + "name": "decided_by",
533 + "type": "text",
534 + "primaryKey": false,
535 + "notNull": false
536 + },
537 + "decided_at": {
538 + "name": "decided_at",
539 + "type": "text",
540 + "primaryKey": false,
541 + "notNull": false
542 + },
543 + "created_at": {
544 + "name": "created_at",
545 + "type": "timestamp with time zone",
546 + "primaryKey": false,
547 + "notNull": true,
548 + "default": "now()"
549 + }
550 + },
551 + "indexes": {},
552 + "foreignKeys": {},
553 + "compositePrimaryKeys": {},
554 + "uniqueConstraints": {},
555 + "policies": {},
556 + "checkConstraints": {},
557 + "isRLSEnabled": false
558 + },
559 + "public.connector_health": {
560 + "name": "connector_health",
561 + "schema": "",
562 + "columns": {
563 + "connector_id": {
564 + "name": "connector_id",
565 + "type": "text",
566 + "primaryKey": true,
567 + "notNull": true
568 + },
569 + "computed_at": {
570 + "name": "computed_at",
571 + "type": "timestamp with time zone",
572 + "primaryKey": false,
573 + "notNull": true
574 + },
575 + "status": {
576 + "name": "status",
577 + "type": "text",
578 + "primaryKey": false,
579 + "notNull": true
580 + },
581 + "health": {
582 + "name": "health",
583 + "type": "jsonb",
584 + "primaryKey": false,
585 + "notNull": true,
586 + "default": "'{}'::jsonb"
587 + }
588 + },
589 + "indexes": {},
590 + "foreignKeys": {},
591 + "compositePrimaryKeys": {},
592 + "uniqueConstraints": {},
593 + "policies": {},
594 + "checkConstraints": {},
595 + "isRLSEnabled": false
596 + },
597 + "public.connector_runs": {
598 + "name": "connector_runs",
599 + "schema": "",
600 + "columns": {
601 + "id": {
602 + "name": "id",
603 + "type": "text",
604 + "primaryKey": true,
605 + "notNull": true
606 + },
607 + "connector_id": {
608 + "name": "connector_id",
609 + "type": "text",
610 + "primaryKey": false,
611 + "notNull": true
612 + },
613 + "trigger": {
614 + "name": "trigger",
615 + "type": "text",
616 + "primaryKey": false,
617 + "notNull": true,
618 + "default": "'schedule'"
619 + },
620 + "started_at": {
621 + "name": "started_at",
622 + "type": "timestamp with time zone",
623 + "primaryKey": false,
624 + "notNull": true
625 + },
626 + "finished_at": {
627 + "name": "finished_at",
628 + "type": "timestamp with time zone",
629 + "primaryKey": false,
630 + "notNull": false
631 + },
632 + "status": {
633 + "name": "status",
634 + "type": "text",
635 + "primaryKey": false,
636 + "notNull": true,
637 + "default": "'running'"
638 + },
639 + "pages_attempted": {
640 + "name": "pages_attempted",
641 + "type": "integer",
642 + "primaryKey": false,
643 + "notNull": true,
644 + "default": 0
645 + },
646 + "pages_success": {
647 + "name": "pages_success",
648 + "type": "integer",
649 + "primaryKey": false,
650 + "notNull": true,
651 + "default": 0
652 + },
653 + "records_raw": {
654 + "name": "records_raw",
655 + "type": "integer",
656 + "primaryKey": false,
657 + "notNull": true,
658 + "default": 0
659 + },
660 + "records_normalized": {
661 + "name": "records_normalized",
662 + "type": "integer",
663 + "primaryKey": false,
664 + "notNull": true,
665 + "default": 0
666 + },
667 + "records_duplicate": {
668 + "name": "records_duplicate",
669 + "type": "integer",
670 + "primaryKey": false,
671 + "notNull": true,
672 + "default": 0
673 + },
674 + "records_rejected": {
675 + "name": "records_rejected",
676 + "type": "integer",
677 + "primaryKey": false,
678 + "notNull": true,
679 + "default": 0
680 + },
681 + "engine_stats": {
682 + "name": "engine_stats",
683 + "type": "jsonb",
684 + "primaryKey": false,
685 + "notNull": true,
686 + "default": "'{}'::jsonb"
687 + },
688 + "anomalies": {
689 + "name": "anomalies",
690 + "type": "jsonb",
691 + "primaryKey": false,
692 + "notNull": true,
693 + "default": "'[]'::jsonb"
694 + },
695 + "error": {
696 + "name": "error",
697 + "type": "text",
698 + "primaryKey": false,
699 + "notNull": false
700 + },
701 + "cost_credits": {
702 + "name": "cost_credits",
703 + "type": "real",
704 + "primaryKey": false,
705 + "notNull": true,
706 + "default": 0
707 + },
708 + "cost_usd_est": {
709 + "name": "cost_usd_est",
710 + "type": "real",
711 + "primaryKey": false,
712 + "notNull": true,
713 + "default": 0
714 + },
715 + "cursor": {
716 + "name": "cursor",
717 + "type": "jsonb",
718 + "primaryKey": false,
719 + "notNull": true,
720 + "default": "'{}'::jsonb"
721 + }
722 + },
723 + "indexes": {
724 + "connector_runs_connector_started_idx": {
725 + "name": "connector_runs_connector_started_idx",
726 + "columns": [
727 + {
728 + "expression": "connector_id",
729 + "isExpression": false,
730 + "asc": true,
731 + "nulls": "last"
732 + },
733 + {
734 + "expression": "started_at",
735 + "isExpression": false,
736 + "asc": true,
737 + "nulls": "last"
738 + }
739 + ],
740 + "isUnique": false,
741 + "concurrently": false,
742 + "method": "btree",
743 + "with": {}
744 + }
745 + },
746 + "foreignKeys": {},
747 + "compositePrimaryKeys": {},
748 + "uniqueConstraints": {},
749 + "policies": {},
750 + "checkConstraints": {},
751 + "isRLSEnabled": false
752 + },
753 + "public.connectors": {
754 + "name": "connectors",
755 + "schema": "",
756 + "columns": {
757 + "id": {
758 + "name": "id",
759 + "type": "text",
760 + "primaryKey": true,
761 + "notNull": true
762 + },
763 + "source_id": {
764 + "name": "source_id",
765 + "type": "text",
766 + "primaryKey": false,
767 + "notNull": true
768 + },
769 + "display_name": {
770 + "name": "display_name",
771 + "type": "text",
772 + "primaryKey": false,
773 + "notNull": true
774 + },
775 + "engine_priority": {
776 + "name": "engine_priority",
777 + "type": "text[]",
778 + "primaryKey": false,
779 + "notNull": true,
780 + "default": "'{}'::text[]"
781 + },
782 + "categories": {
783 + "name": "categories",
784 + "type": "text[]",
785 + "primaryKey": false,
786 + "notNull": true,
787 + "default": "'{}'::text[]"
788 + },
789 + "regions": {
790 + "name": "regions",
791 + "type": "text[]",
792 + "primaryKey": false,
793 + "notNull": true,
794 + "default": "'{}'::text[]"
795 + },
796 + "languages": {
797 + "name": "languages",
798 + "type": "text[]",
799 + "primaryKey": false,
800 + "notNull": true,
801 + "default": "'{}'::text[]"
802 + },
803 + "currency": {
804 + "name": "currency",
805 + "type": "text[]",
806 + "primaryKey": false,
807 + "notNull": true,
808 + "default": "'{}'::text[]"
809 + },
810 + "supports_listings": {
811 + "name": "supports_listings",
812 + "type": "boolean",
813 + "primaryKey": false,
814 + "notNull": true,
815 + "default": false
816 + },
817 + "supports_sold": {
818 + "name": "supports_sold",
819 + "type": "boolean",
820 + "primaryKey": false,
821 + "notNull": true,
822 + "default": false
823 + },
824 + "supports_auctions": {
825 + "name": "supports_auctions",
826 + "type": "boolean",
827 + "primaryKey": false,
828 + "notNull": true,
829 + "default": false
830 + },
831 + "supports_images": {
832 + "name": "supports_images",
833 + "type": "boolean",
834 + "primaryKey": false,
835 + "notNull": true,
836 + "default": true
837 + },
838 + "supports_catalog": {
839 + "name": "supports_catalog",
840 + "type": "boolean",
841 + "primaryKey": false,
842 + "notNull": true,
843 + "default": false
844 + },
845 + "supports_population": {
846 + "name": "supports_population",
847 + "type": "boolean",
848 + "primaryKey": false,
849 + "notNull": true,
850 + "default": false
851 + },
852 + "refresh_frequency_minutes": {
853 + "name": "refresh_frequency_minutes",
854 + "type": "integer",
855 + "primaryKey": false,
856 + "notNull": true,
857 + "default": 1440
858 + },
859 + "priority": {
860 + "name": "priority",
861 + "type": "text",
862 + "primaryKey": false,
863 + "notNull": true,
864 + "default": "'medium'"
865 + },
866 + "status": {
867 + "name": "status",
868 + "type": "text",
869 + "primaryKey": false,
870 + "notNull": true,
871 + "default": "'active'"
872 + },
873 + "schema_version": {
874 + "name": "schema_version",
875 + "type": "text",
876 + "primaryKey": false,
877 + "notNull": true,
878 + "default": "'1.0'"
879 + },
880 + "connector_version": {
881 + "name": "connector_version",
882 + "type": "text",
883 + "primaryKey": false,
884 + "notNull": true,
885 + "default": "'1.0.0'"
886 + },
887 + "config": {
888 + "name": "config",
889 + "type": "jsonb",
890 + "primaryKey": false,
891 + "notNull": true,
892 + "default": "'{}'::jsonb"
893 + },
894 + "last_run_at": {
895 + "name": "last_run_at",
896 + "type": "timestamp with time zone",
897 + "primaryKey": false,
898 + "notNull": false
899 + },
900 + "last_success_at": {
901 + "name": "last_success_at",
902 + "type": "timestamp with time zone",
903 + "primaryKey": false,
904 + "notNull": false
905 + },
906 + "next_run_at": {
907 + "name": "next_run_at",
908 + "type": "timestamp with time zone",
909 + "primaryKey": false,
910 + "notNull": false
911 + },
912 + "created_at": {
913 + "name": "created_at",
914 + "type": "timestamp with time zone",
915 + "primaryKey": false,
916 + "notNull": true,
917 + "default": "now()"
918 + },
919 + "updated_at": {
920 + "name": "updated_at",
921 + "type": "timestamp with time zone",
922 + "primaryKey": false,
923 + "notNull": true,
924 + "default": "now()"
925 + }
926 + },
927 + "indexes": {},
928 + "foreignKeys": {},
929 + "compositePrimaryKeys": {},
930 + "uniqueConstraints": {},
931 + "policies": {},
932 + "checkConstraints": {},
933 + "isRLSEnabled": false
934 + },
935 + "public.costs": {
936 + "name": "costs",
937 + "schema": "",
938 + "columns": {
939 + "id": {
940 + "name": "id",
941 + "type": "text",
942 + "primaryKey": true,
943 + "notNull": true
944 + },
945 + "occurred_at": {
946 + "name": "occurred_at",
947 + "type": "timestamp with time zone",
948 + "primaryKey": false,
949 + "notNull": true
950 + },
951 + "kind": {
952 + "name": "kind",
953 + "type": "text",
954 + "primaryKey": false,
955 + "notNull": true
956 + },
957 + "provider": {
958 + "name": "provider",
959 + "type": "text",
960 + "primaryKey": false,
961 + "notNull": false
962 + },
963 + "connector_id": {
964 + "name": "connector_id",
965 + "type": "text",
966 + "primaryKey": false,
967 + "notNull": false
968 + },
969 + "category_slug": {
970 + "name": "category_slug",
971 + "type": "text",
972 + "primaryKey": false,
973 + "notNull": false
974 + },
975 + "endpoint": {
976 + "name": "endpoint",
977 + "type": "text",
978 + "primaryKey": false,
979 + "notNull": false
980 + },
981 + "user_id": {
982 + "name": "user_id",
983 + "type": "text",
984 + "primaryKey": false,
985 + "notNull": false
986 + },
987 + "units": {
988 + "name": "units",
989 + "type": "real",
990 + "primaryKey": false,
991 + "notNull": true,
992 + "default": 1
993 + },
994 + "credits": {
995 + "name": "credits",
996 + "type": "real",
997 + "primaryKey": false,
998 + "notNull": true,
999 + "default": 0
1000 + },
1001 + "usd_est": {
1002 + "name": "usd_est",
1003 + "type": "real",
1004 + "primaryKey": false,
1005 + "notNull": true,
1006 + "default": 0
1007 + },
1008 + "metadata": {
1009 + "name": "metadata",
1010 + "type": "jsonb",
1011 + "primaryKey": false,
1012 + "notNull": true,
1013 + "default": "'{}'::jsonb"
1014 + }
1015 + },
1016 + "indexes": {
1017 + "costs_occurred_idx": {
1018 + "name": "costs_occurred_idx",
1019 + "columns": [
1020 + {
1021 + "expression": "occurred_at",
1022 + "isExpression": false,
1023 + "asc": true,
1024 + "nulls": "last"
1025 + }
1026 + ],
1027 + "isUnique": false,
1028 + "concurrently": false,
1029 + "method": "btree",
1030 + "with": {}
1031 + },
1032 + "costs_connector_idx": {
1033 + "name": "costs_connector_idx",
1034 + "columns": [
1035 + {
1036 + "expression": "connector_id",
1037 + "isExpression": false,
1038 + "asc": true,
1039 + "nulls": "last"
1040 + }
1041 + ],
1042 + "isUnique": false,
1043 + "concurrently": false,
1044 + "method": "btree",
1045 + "with": {}
1046 + }
1047 + },
1048 + "foreignKeys": {},
1049 + "compositePrimaryKeys": {},
1050 + "uniqueConstraints": {},
1051 + "policies": {},
1052 + "checkConstraints": {},
1053 + "isRLSEnabled": false
1054 + },
1055 + "public.crawl_state": {
1056 + "name": "crawl_state",
1057 + "schema": "",
1058 + "columns": {
1059 + "url_hash": {
1060 + "name": "url_hash",
1061 + "type": "text",
1062 + "primaryKey": true,
1063 + "notNull": true
1064 + },
1065 + "connector_id": {
1066 + "name": "connector_id",
1067 + "type": "text",
1068 + "primaryKey": false,
1069 + "notNull": true
1070 + },
1071 + "url": {
1072 + "name": "url",
1073 + "type": "text",
1074 + "primaryKey": false,
1075 + "notNull": true
1076 + },
1077 + "etag": {
1078 + "name": "etag",
1079 + "type": "text",
1080 + "primaryKey": false,
1081 + "notNull": false
1082 + },
1083 + "last_modified": {
1084 + "name": "last_modified",
1085 + "type": "text",
1086 + "primaryKey": false,
1087 + "notNull": false
1088 + },
1089 + "content_hash": {
1090 + "name": "content_hash",
1091 + "type": "text",
1092 + "primaryKey": false,
1093 + "notNull": false
1094 + },
1095 + "last_fetched_at": {
1096 + "name": "last_fetched_at",
1097 + "type": "timestamp with time zone",
1098 + "primaryKey": false,
1099 + "notNull": false
1100 + },
1101 + "last_changed_at": {
1102 + "name": "last_changed_at",
1103 + "type": "timestamp with time zone",
1104 + "primaryKey": false,
1105 + "notNull": false
1106 + },
1107 + "fetch_count": {
1108 + "name": "fetch_count",
1109 + "type": "integer",
1110 + "primaryKey": false,
1111 + "notNull": true,
1112 + "default": 0
1113 + },
1114 + "change_count": {
1115 + "name": "change_count",
1116 + "type": "integer",
1117 + "primaryKey": false,
1118 + "notNull": true,
1119 + "default": 0
1120 + },
1121 + "change_interval_hours": {
1122 + "name": "change_interval_hours",
1123 + "type": "real",
1124 + "primaryKey": false,
1125 + "notNull": false
1126 + },
1127 + "next_fetch_at": {
1128 + "name": "next_fetch_at",
1129 + "type": "timestamp with time zone",
1130 + "primaryKey": false,
1131 + "notNull": false
1132 + },
1133 + "last_status": {
1134 + "name": "last_status",
1135 + "type": "integer",
1136 + "primaryKey": false,
1137 + "notNull": false
1138 + },
1139 + "failures": {
1140 + "name": "failures",
1141 + "type": "integer",
1142 + "primaryKey": false,
1143 + "notNull": true,
1144 + "default": 0
1145 + }
1146 + },
1147 + "indexes": {
1148 + "crawl_state_next_idx": {
1149 + "name": "crawl_state_next_idx",
1150 + "columns": [
1151 + {
1152 + "expression": "connector_id",
1153 + "isExpression": false,
1154 + "asc": true,
1155 + "nulls": "last"
1156 + },
1157 + {
1158 + "expression": "next_fetch_at",
1159 + "isExpression": false,
1160 + "asc": true,
1161 + "nulls": "last"
1162 + }
1163 + ],
1164 + "isUnique": false,
1165 + "concurrently": false,
1166 + "method": "btree",
1167 + "with": {}
1168 + }
1169 + },
1170 + "foreignKeys": {},
1171 + "compositePrimaryKeys": {},
1172 + "uniqueConstraints": {},
1173 + "policies": {},
1174 + "checkConstraints": {},
1175 + "isRLSEnabled": false
1176 + },
1177 + "public.sources": {
1178 + "name": "sources",
1179 + "schema": "",
1180 + "columns": {
1181 + "id": {
1182 + "name": "id",
1183 + "type": "text",
1184 + "primaryKey": true,
1185 + "notNull": true
1186 + },
1187 + "name": {
1188 + "name": "name",
1189 + "type": "text",
1190 + "primaryKey": false,
1191 + "notNull": true
1192 + },
1193 + "source_type": {
1194 + "name": "source_type",
1195 + "type": "text",
1196 + "primaryKey": false,
1197 + "notNull": true
1198 + },
1199 + "url": {
1200 + "name": "url",
1201 + "type": "text",
1202 + "primaryKey": false,
1203 + "notNull": false
1204 + },
1205 + "countries": {
1206 + "name": "countries",
1207 + "type": "text[]",
1208 + "primaryKey": false,
1209 + "notNull": true,
1210 + "default": "'{}'::text[]"
1211 + },
1212 + "languages": {
1213 + "name": "languages",
1214 + "type": "text[]",
1215 + "primaryKey": false,
1216 + "notNull": true,
1217 + "default": "'{}'::text[]"
1218 + },
1219 + "currencies": {
1220 + "name": "currencies",
1221 + "type": "text[]",
1222 + "primaryKey": false,
1223 + "notNull": true,
1224 + "default": "'{}'::text[]"
1225 + },
1226 + "trust_score": {
1227 + "name": "trust_score",
1228 + "type": "numeric(8, 6)",
1229 + "primaryKey": false,
1230 + "notNull": true,
1231 + "default": 0.5
1232 + },
1233 + "trust_factors": {
1234 + "name": "trust_factors",
1235 + "type": "jsonb",
1236 + "primaryKey": false,
1237 + "notNull": true,
1238 + "default": "'{}'::jsonb"
1239 + },
1240 + "attribution_required": {
1241 + "name": "attribution_required",
1242 + "type": "boolean",
1243 + "primaryKey": false,
1244 + "notNull": true,
1245 + "default": true
1246 + },
1247 + "terms_url": {
1248 + "name": "terms_url",
1249 + "type": "text",
1250 + "primaryKey": false,
1251 + "notNull": false
1252 + },
1253 + "active": {
1254 + "name": "active",
1255 + "type": "boolean",
1256 + "primaryKey": false,
1257 + "notNull": true,
1258 + "default": true
1259 + },
1260 + "created_at": {
1261 + "name": "created_at",
1262 + "type": "timestamp with time zone",
1263 + "primaryKey": false,
1264 + "notNull": true,
1265 + "default": "now()"
1266 + },
1267 + "updated_at": {
1268 + "name": "updated_at",
1269 + "type": "timestamp with time zone",
1270 + "primaryKey": false,
1271 + "notNull": true,
1272 + "default": "now()"
1273 + }
1274 + },
1275 + "indexes": {},
1276 + "foreignKeys": {},
1277 + "compositePrimaryKeys": {},
1278 + "uniqueConstraints": {},
1279 + "policies": {},
1280 + "checkConstraints": {},
1281 + "isRLSEnabled": false
1282 + },
1283 + "public.audit_log": {
1284 + "name": "audit_log",
1285 + "schema": "",
1286 + "columns": {
1287 + "id": {
1288 + "name": "id",
1289 + "type": "text",
1290 + "primaryKey": true,
1291 + "notNull": true
1292 + },
1293 + "entity_type": {
1294 + "name": "entity_type",
1295 + "type": "text",
1296 + "primaryKey": false,
1297 + "notNull": true
1298 + },
1299 + "entity_id": {
1300 + "name": "entity_id",
1301 + "type": "text",
1302 + "primaryKey": false,
1303 + "notNull": true
1304 + },
1305 + "action": {
1306 + "name": "action",
1307 + "type": "text",
1308 + "primaryKey": false,
1309 + "notNull": true
1310 + },
1311 + "reason": {
1312 + "name": "reason",
1313 + "type": "text",
1314 + "primaryKey": false,
1315 + "notNull": true
1316 + },
1317 + "actor": {
1318 + "name": "actor",
1319 + "type": "text",
1320 + "primaryKey": false,
1321 + "notNull": true,
1322 + "default": "'system'"
1323 + },
1324 + "details": {
1325 + "name": "details",
1326 + "type": "jsonb",
1327 + "primaryKey": false,
1328 + "notNull": true,
1329 + "default": "'{}'::jsonb"
1330 + },
1331 + "created_at": {
1332 + "name": "created_at",
1333 + "type": "timestamp with time zone",
1334 + "primaryKey": false,
1335 + "notNull": true,
1336 + "default": "now()"
1337 + }
1338 + },
1339 + "indexes": {
1340 + "audit_entity_idx": {
1341 + "name": "audit_entity_idx",
1342 + "columns": [
1343 + {
1344 + "expression": "entity_type",
1345 + "isExpression": false,
1346 + "asc": true,
1347 + "nulls": "last"
1348 + },
1349 + {
1350 + "expression": "entity_id",
1351 + "isExpression": false,
1352 + "asc": true,
1353 + "nulls": "last"
1354 + }
1355 + ],
1356 + "isUnique": false,
1357 + "concurrently": false,
1358 + "method": "btree",
1359 + "with": {}
1360 + }
1361 + },
1362 + "foreignKeys": {},
1363 + "compositePrimaryKeys": {},
1364 + "uniqueConstraints": {},
1365 + "policies": {},
1366 + "checkConstraints": {},
1367 + "isRLSEnabled": false
1368 + },
1369 + "public.events": {
1370 + "name": "events",
1371 + "schema": "",
1372 + "columns": {
1373 + "id": {
1374 + "name": "id",
1375 + "type": "text",
1376 + "primaryKey": true,
1377 + "notNull": true
1378 + },
1379 + "type": {
1380 + "name": "type",
1381 + "type": "text",
1382 + "primaryKey": false,
1383 + "notNull": true
1384 + },
1385 + "entity_type": {
1386 + "name": "entity_type",
1387 + "type": "text",
1388 + "primaryKey": false,
1389 + "notNull": false
1390 + },
1391 + "entity_id": {
1392 + "name": "entity_id",
1393 + "type": "text",
1394 + "primaryKey": false,
1395 + "notNull": false
1396 + },
1397 + "payload": {
1398 + "name": "payload",
1399 + "type": "jsonb",
1400 + "primaryKey": false,
1401 + "notNull": true,
1402 + "default": "'{}'::jsonb"
1403 + },
1404 + "created_at": {
1405 + "name": "created_at",
1406 + "type": "timestamp with time zone",
1407 + "primaryKey": false,
1408 + "notNull": true,
1409 + "default": "now()"
1410 + }
1411 + },
1412 + "indexes": {
1413 + "events_type_created_idx": {
1414 + "name": "events_type_created_idx",
1415 + "columns": [
1416 + {
1417 + "expression": "type",
1418 + "isExpression": false,
1419 + "asc": true,
1420 + "nulls": "last"
1421 + },
1422 + {
1423 + "expression": "created_at",
1424 + "isExpression": false,
1425 + "asc": true,
1426 + "nulls": "last"
1427 + }
1428 + ],
1429 + "isUnique": false,
1430 + "concurrently": false,
1431 + "method": "btree",
1432 + "with": {}
1433 + },
1434 + "events_entity_idx": {
1435 + "name": "events_entity_idx",
1436 + "columns": [
1437 + {
1438 + "expression": "entity_type",
1439 + "isExpression": false,
1440 + "asc": true,
1441 + "nulls": "last"
1442 + },
1443 + {
1444 + "expression": "entity_id",
1445 + "isExpression": false,
1446 + "asc": true,
1447 + "nulls": "last"
1448 + }
1449 + ],
1450 + "isUnique": false,
1451 + "concurrently": false,
1452 + "method": "btree",
1453 + "with": {}
1454 + }
1455 + },
1456 + "foreignKeys": {},
1457 + "compositePrimaryKeys": {},
1458 + "uniqueConstraints": {},
1459 + "policies": {},
1460 + "checkConstraints": {},
1461 + "isRLSEnabled": false
1462 + },
1463 + "public.normalized_records": {
1464 + "name": "normalized_records",
1465 + "schema": "",
1466 + "columns": {
1467 + "id": {
1468 + "name": "id",
1469 + "type": "text",
1470 + "primaryKey": true,
1471 + "notNull": true
1472 + },
1473 + "raw_record_id": {
1474 + "name": "raw_record_id",
1475 + "type": "text",
1476 + "primaryKey": false,
1477 + "notNull": true
1478 + },
1479 + "connector_id": {
1480 + "name": "connector_id",
1481 + "type": "text",
1482 + "primaryKey": false,
1483 + "notNull": true
1484 + },
1485 + "source_id": {
1486 + "name": "source_id",
1487 + "type": "text",
1488 + "primaryKey": false,
1489 + "notNull": true
1490 + },
1491 + "kind": {
1492 + "name": "kind",
1493 + "type": "text",
1494 + "primaryKey": false,
1495 + "notNull": true
1496 + },
1497 + "payload": {
1498 + "name": "payload",
1499 + "type": "jsonb",
1500 + "primaryKey": false,
1501 + "notNull": true
1502 + },
1503 + "seq": {
1504 + "name": "seq",
1505 + "type": "integer",
1506 + "primaryKey": false,
1507 + "notNull": true,
1508 + "default": 0
1509 + },
1510 + "asset_id": {
1511 + "name": "asset_id",
1512 + "type": "text",
1513 + "primaryKey": false,
1514 + "notNull": false
1515 + },
1516 + "variant_id": {
1517 + "name": "variant_id",
1518 + "type": "text",
1519 + "primaryKey": false,
1520 + "notNull": false
1521 + },
1522 + "match_method": {
1523 + "name": "match_method",
1524 + "type": "text",
1525 + "primaryKey": false,
1526 + "notNull": false
1527 + },
1528 + "match_confidence": {
1529 + "name": "match_confidence",
1530 + "type": "numeric(8, 6)",
1531 + "primaryKey": false,
1532 + "notNull": false
1533 + },
1534 + "status": {
1535 + "name": "status",
1536 + "type": "text",
1537 + "primaryKey": false,
1538 + "notNull": true,
1539 + "default": "'pending'"
1540 + },
1541 + "reject_reason": {
1542 + "name": "reject_reason",
1543 + "type": "text",
1544 + "primaryKey": false,
1545 + "notNull": false
1546 + },
1547 + "target_id": {
1548 + "name": "target_id",
1549 + "type": "text",
1550 + "primaryKey": false,
1551 + "notNull": false
1552 + },
1553 + "created_at": {
1554 + "name": "created_at",
1555 + "type": "timestamp with time zone",
1556 + "primaryKey": false,
1557 + "notNull": true,
1558 + "default": "now()"
1559 + },
1560 + "processed_at": {
1561 + "name": "processed_at",
1562 + "type": "timestamp with time zone",
1563 + "primaryKey": false,
1564 + "notNull": false
1565 + }
1566 + },
1567 + "indexes": {
1568 + "normalized_records_status_idx": {
1569 + "name": "normalized_records_status_idx",
1570 + "columns": [
1571 + {
1572 + "expression": "status",
1573 + "isExpression": false,
1574 + "asc": true,
1575 + "nulls": "last"
1576 + },
1577 + {
1578 + "expression": "created_at",
1579 + "isExpression": false,
1580 + "asc": true,
1581 + "nulls": "last"
1582 + }
1583 + ],
1584 + "isUnique": false,
1585 + "concurrently": false,
1586 + "method": "btree",
1587 + "with": {}
1588 + },
1589 + "normalized_records_status_kind_idx": {
1590 + "name": "normalized_records_status_kind_idx",
1591 + "columns": [
1592 + {
1593 + "expression": "status",
1594 + "isExpression": false,
1595 + "asc": true,
1596 + "nulls": "last"
1597 + },
1598 + {
1599 + "expression": "kind",
1600 + "isExpression": false,
1601 + "asc": true,
1602 + "nulls": "last"
1603 + },
1604 + {
1605 + "expression": "created_at",
1606 + "isExpression": false,
1607 + "asc": true,
1608 + "nulls": "last"
1609 + }
1610 + ],
1611 + "isUnique": false,
1612 + "concurrently": false,
1613 + "method": "btree",
1614 + "with": {}
1615 + },
1616 + "normalized_records_asset_idx": {
1617 + "name": "normalized_records_asset_idx",
1618 + "columns": [
1619 + {
1620 + "expression": "asset_id",
1621 + "isExpression": false,
1622 + "asc": true,
1623 + "nulls": "last"
1624 + }
1625 + ],
1626 + "isUnique": false,
1627 + "concurrently": false,
1628 + "method": "btree",
1629 + "with": {}
1630 + },
1631 + "normalized_records_raw_uq": {
1632 + "name": "normalized_records_raw_uq",
1633 + "columns": [
1634 + {
1635 + "expression": "raw_record_id",
1636 + "isExpression": false,
1637 + "asc": true,
1638 + "nulls": "last"
1639 + },
1640 + {
1641 + "expression": "kind",
1642 + "isExpression": false,
1643 + "asc": true,
1644 + "nulls": "last"
1645 + },
1646 + {
1647 + "expression": "seq",
1648 + "isExpression": false,
1649 + "asc": true,
1650 + "nulls": "last"
1651 + }
1652 + ],
1653 + "isUnique": true,
1654 + "concurrently": false,
1655 + "method": "btree",
1656 + "with": {}
1657 + }
1658 + },
1659 + "foreignKeys": {},
1660 + "compositePrimaryKeys": {},
1661 + "uniqueConstraints": {},
1662 + "policies": {},
1663 + "checkConstraints": {},
1664 + "isRLSEnabled": false
1665 + },
1666 + "public.raw_records": {
1667 + "name": "raw_records",
1668 + "schema": "",
1669 + "columns": {
1670 + "id": {
1671 + "name": "id",
1672 + "type": "text",
1673 + "primaryKey": true,
1674 + "notNull": true
1675 + },
1676 + "connector_id": {
1677 + "name": "connector_id",
1678 + "type": "text",
1679 + "primaryKey": false,
1680 + "notNull": true
1681 + },
1682 + "source_id": {
1683 + "name": "source_id",
1684 + "type": "text",
1685 + "primaryKey": false,
1686 + "notNull": true
1687 + },
1688 + "run_id": {
1689 + "name": "run_id",
1690 + "type": "text",
1691 + "primaryKey": false,
1692 + "notNull": false
1693 + },
1694 + "engine": {
1695 + "name": "engine",
1696 + "type": "text",
1697 + "primaryKey": false,
1698 + "notNull": true
1699 + },
1700 + "url": {
1701 + "name": "url",
1702 + "type": "text",
1703 + "primaryKey": false,
1704 + "notNull": true
1705 + },
1706 + "external_id": {
1707 + "name": "external_id",
1708 + "type": "text",
1709 + "primaryKey": false,
1710 + "notNull": false
1711 + },
1712 + "kind": {
1713 + "name": "kind",
1714 + "type": "text",
1715 + "primaryKey": false,
1716 + "notNull": true
1717 + },
1718 + "fetched_at": {
1719 + "name": "fetched_at",
1720 + "type": "timestamp with time zone",
1721 + "primaryKey": false,
1722 + "notNull": true
1723 + },
1724 + "content_hash": {
1725 + "name": "content_hash",
1726 + "type": "text",
1727 + "primaryKey": false,
1728 + "notNull": true
1729 + },
1730 + "http_status": {
1731 + "name": "http_status",
1732 + "type": "integer",
1733 + "primaryKey": false,
1734 + "notNull": false
1735 + },
1736 + "payload": {
1737 + "name": "payload",
1738 + "type": "jsonb",
1739 + "primaryKey": false,
1740 + "notNull": true
1741 + },
1742 + "snapshot_ref": {
1743 + "name": "snapshot_ref",
1744 + "type": "text",
1745 + "primaryKey": false,
1746 + "notNull": false
1747 + },
1748 + "parser_version": {
1749 + "name": "parser_version",
1750 + "type": "text",
1751 + "primaryKey": false,
1752 + "notNull": true
1753 + },
1754 + "connector_version": {
1755 + "name": "connector_version",
1756 + "type": "text",
1757 + "primaryKey": false,
1758 + "notNull": true
1759 + },
1760 + "processed_at": {
1761 + "name": "processed_at",
1762 + "type": "timestamp with time zone",
1763 + "primaryKey": false,
1764 + "notNull": false
1765 + },
1766 + "process_error": {
1767 + "name": "process_error",
1768 + "type": "text",
1769 + "primaryKey": false,
1770 + "notNull": false
1771 + },
1772 + "created_at": {
1773 + "name": "created_at",
1774 + "type": "timestamp with time zone",
1775 + "primaryKey": false,
1776 + "notNull": true,
1777 + "default": "now()"
1778 + }
1779 + },
1780 + "indexes": {
1781 + "raw_records_connector_hash_uq": {
1782 + "name": "raw_records_connector_hash_uq",
1783 + "columns": [
1784 + {
1785 + "expression": "connector_id",
1786 + "isExpression": false,
1787 + "asc": true,
1788 + "nulls": "last"
1789 + },
1790 + {
1791 + "expression": "content_hash",
1792 + "isExpression": false,
1793 + "asc": true,
1794 + "nulls": "last"
1795 + }
1796 + ],
1797 + "isUnique": true,
1798 + "concurrently": false,
1799 + "method": "btree",
1800 + "with": {}
1801 + },
1802 + "raw_records_connector_fetched_idx": {
1803 + "name": "raw_records_connector_fetched_idx",
1804 + "columns": [
1805 + {
1806 + "expression": "connector_id",
1807 + "isExpression": false,
1808 + "asc": true,
1809 + "nulls": "last"
1810 + },
1811 + {
1812 + "expression": "fetched_at",
1813 + "isExpression": false,
1814 + "asc": true,
1815 + "nulls": "last"
1816 + }
1817 + ],
1818 + "isUnique": false,
1819 + "concurrently": false,
1820 + "method": "btree",
1821 + "with": {}
1822 + },
1823 + "raw_records_unprocessed_idx": {
1824 + "name": "raw_records_unprocessed_idx",
1825 + "columns": [
1826 + {
1827 + "expression": "processed_at",
1828 + "isExpression": false,
1829 + "asc": true,
1830 + "nulls": "last"
1831 + }
1832 + ],
1833 + "isUnique": false,
1834 + "concurrently": false,
1835 + "method": "btree",
1836 + "with": {}
1837 + },
1838 + "raw_records_external_idx": {
1839 + "name": "raw_records_external_idx",
1840 + "columns": [
1841 + {
1842 + "expression": "connector_id",
1843 + "isExpression": false,
1844 + "asc": true,
1845 + "nulls": "last"
1846 + },
1847 + {
1848 + "expression": "external_id",
1849 + "isExpression": false,
1850 + "asc": true,
1851 + "nulls": "last"
1852 + }
1853 + ],
1854 + "isUnique": false,
1855 + "concurrently": false,
1856 + "method": "btree",
1857 + "with": {}
1858 + }
1859 + },
1860 + "foreignKeys": {},
1861 + "compositePrimaryKeys": {},
1862 + "uniqueConstraints": {},
1863 + "policies": {},
1864 + "checkConstraints": {},
1865 + "isRLSEnabled": false
1866 + },
1867 + "public.asset_embeddings": {
1868 + "name": "asset_embeddings",
1869 + "schema": "",
1870 + "columns": {
1871 + "asset_id": {
1872 + "name": "asset_id",
1873 + "type": "text",
1874 + "primaryKey": true,
1875 + "notNull": true
1876 + },
1877 + "model": {
1878 + "name": "model",
1879 + "type": "text",
1880 + "primaryKey": false,
1881 + "notNull": true
1882 + },
1883 + "embedding": {
1884 + "name": "embedding",
1885 + "type": "vector(1536)",
1886 + "primaryKey": false,
1887 + "notNull": true
1888 + },
1889 + "updated_at": {
1890 + "name": "updated_at",
1891 + "type": "timestamp with time zone",
1892 + "primaryKey": false,
1893 + "notNull": true,
1894 + "default": "now()"
1895 + }
1896 + },
1897 + "indexes": {},
1898 + "foreignKeys": {},
1899 + "compositePrimaryKeys": {},
1900 + "uniqueConstraints": {},
1901 + "policies": {},
1902 + "checkConstraints": {},
1903 + "isRLSEnabled": false
1904 + },
1905 + "public.asset_stats": {
1906 + "name": "asset_stats",
1907 + "schema": "",
1908 + "columns": {
1909 + "asset_id": {
1910 + "name": "asset_id",
1911 + "type": "text",
1912 + "primaryKey": true,
1913 + "notNull": true
1914 + },
1915 + "riv_usd": {
1916 + "name": "riv_usd",
1917 + "type": "numeric(18, 4)",
1918 + "primaryKey": false,
1919 + "notNull": false
1920 + },
1921 + "riv_low_usd": {
1922 + "name": "riv_low_usd",
1923 + "type": "numeric(18, 4)",
1924 + "primaryKey": false,
1925 + "notNull": false
1926 + },
1927 + "riv_high_usd": {
1928 + "name": "riv_high_usd",
1929 + "type": "numeric(18, 4)",
1930 + "primaryKey": false,
1931 + "notNull": false
1932 + },
1933 + "riv_confidence": {
1934 + "name": "riv_confidence",
1935 + "type": "numeric(8, 6)",
1936 + "primaryKey": false,
1937 + "notNull": false
1938 + },
1939 + "riv_sample_size": {
1940 + "name": "riv_sample_size",
1941 + "type": "integer",
1942 + "primaryKey": false,
1943 + "notNull": true,
1944 + "default": 0
1945 + },
1946 + "riv_variant_id": {
1947 + "name": "riv_variant_id",
1948 + "type": "text",
1949 + "primaryKey": false,
1950 + "notNull": false
1951 + },
1952 + "latest_sale_usd": {
1953 + "name": "latest_sale_usd",
1954 + "type": "numeric(18, 4)",
1955 + "primaryKey": false,
1956 + "notNull": false
1957 + },
1958 + "latest_sale_at": {
1959 + "name": "latest_sale_at",
1960 + "type": "timestamp with time zone",
1961 + "primaryKey": false,
1962 + "notNull": false
1963 + },
1964 + "change_1d": {
1965 + "name": "change_1d",
1966 + "type": "numeric(8, 6)",
1967 + "primaryKey": false,
1968 + "notNull": false
1969 + },
1970 + "change_7d": {
1971 + "name": "change_7d",
1972 + "type": "numeric(8, 6)",
1973 + "primaryKey": false,
1974 + "notNull": false
1975 + },
1976 + "change_30d": {
1977 + "name": "change_30d",
1978 + "type": "numeric(8, 6)",
1979 + "primaryKey": false,
1980 + "notNull": false
1981 + },
1982 + "change_90d": {
1983 + "name": "change_90d",
1984 + "type": "numeric(8, 6)",
1985 + "primaryKey": false,
1986 + "notNull": false
1987 + },
1988 + "change_1y": {
1989 + "name": "change_1y",
1990 + "type": "numeric(8, 6)",
1991 + "primaryKey": false,
1992 + "notNull": false
1993 + },
1994 + "ath_usd": {
1995 + "name": "ath_usd",
1996 + "type": "numeric(18, 4)",
1997 + "primaryKey": false,
1998 + "notNull": false
1999 + },
2000 + "ath_at": {
2001 + "name": "ath_at",
2002 + "type": "timestamp with time zone",
2003 + "primaryKey": false,
2004 + "notNull": false
2005 + },
2006 + "atl_usd": {
2007 + "name": "atl_usd",
2008 + "type": "numeric(18, 4)",
2009 + "primaryKey": false,
2010 + "notNull": false
2011 + },
2012 + "atl_at": {
2013 + "name": "atl_at",
2014 + "type": "timestamp with time zone",
2015 + "primaryKey": false,
2016 + "notNull": false
2017 + },
2018 + "sales_count": {
2019 + "name": "sales_count",
2020 + "type": "integer",
2021 + "primaryKey": false,
2022 + "notNull": true,
2023 + "default": 0
2024 + },
2025 + "sales_30d": {
2026 + "name": "sales_30d",
2027 + "type": "integer",
2028 + "primaryKey": false,
2029 + "notNull": true,
2030 + "default": 0
2031 + },
2032 + "sales_1y": {
2033 + "name": "sales_1y",
2034 + "type": "integer",
2035 + "primaryKey": false,
2036 + "notNull": true,
2037 + "default": 0
2038 + },
2039 + "volume_30d_usd": {
2040 + "name": "volume_30d_usd",
2041 + "type": "numeric(18, 4)",
2042 + "primaryKey": false,
2043 + "notNull": false
2044 + },
2045 + "active_listings": {
2046 + "name": "active_listings",
2047 + "type": "integer",
2048 + "primaryKey": false,
2049 + "notNull": true,
2050 + "default": 0
2051 + },
2052 + "min_ask_usd": {
2053 + "name": "min_ask_usd",
2054 + "type": "numeric(18, 4)",
2055 + "primaryKey": false,
2056 + "notNull": false
2057 + },
2058 + "observations_count": {
2059 + "name": "observations_count",
2060 + "type": "integer",
2061 + "primaryKey": false,
2062 + "notNull": true,
2063 + "default": 0
2064 + },
2065 + "sources_count": {
2066 + "name": "sources_count",
2067 + "type": "integer",
2068 + "primaryKey": false,
2069 + "notNull": true,
2070 + "default": 0
2071 + },
2072 + "liquidity_score": {
2073 + "name": "liquidity_score",
2074 + "type": "real",
2075 + "primaryKey": false,
2076 + "notNull": false
2077 + },
2078 + "rarity_score": {
2079 + "name": "rarity_score",
2080 + "type": "real",
2081 + "primaryKey": false,
2082 + "notNull": false
2083 + },
2084 + "momentum_7d": {
2085 + "name": "momentum_7d",
2086 + "type": "real",
2087 + "primaryKey": false,
2088 + "notNull": false
2089 + },
2090 + "momentum_30d": {
2091 + "name": "momentum_30d",
2092 + "type": "real",
2093 + "primaryKey": false,
2094 + "notNull": false
2095 + },
2096 + "momentum_90d": {
2097 + "name": "momentum_90d",
2098 + "type": "real",
2099 + "primaryKey": false,
2100 + "notNull": false
2101 + },
2102 + "momentum_1y": {
2103 + "name": "momentum_1y",
2104 + "type": "real",
2105 + "primaryKey": false,
2106 + "notNull": false
2107 + },
2108 + "trending_score": {
2109 + "name": "trending_score",
2110 + "type": "real",
2111 + "primaryKey": false,
2112 + "notNull": false
2113 + },
2114 + "value_opportunity": {
2115 + "name": "value_opportunity",
2116 + "type": "real",
2117 + "primaryKey": false,
2118 + "notNull": false
2119 + },
2120 + "data_quality": {
2121 + "name": "data_quality",
2122 + "type": "real",
2123 + "primaryKey": false,
2124 + "notNull": false
2125 + },
2126 + "watchers": {
2127 + "name": "watchers",
2128 + "type": "integer",
2129 + "primaryKey": false,
2130 + "notNull": true,
2131 + "default": 0
2132 + },
2133 + "views_30d": {
2134 + "name": "views_30d",
2135 + "type": "integer",
2136 + "primaryKey": false,
2137 + "notNull": true,
2138 + "default": 0
2139 + },
2140 + "updated_at": {
2141 + "name": "updated_at",
2142 + "type": "timestamp with time zone",
2143 + "primaryKey": false,
2144 + "notNull": true,
2145 + "default": "now()"
2146 + }
2147 + },
2148 + "indexes": {
2149 + "asset_stats_riv_idx": {
2150 + "name": "asset_stats_riv_idx",
2151 + "columns": [
2152 + {
2153 + "expression": "riv_usd",
2154 + "isExpression": false,
2155 + "asc": true,
2156 + "nulls": "last"
2157 + }
2158 + ],
2159 + "isUnique": false,
2160 + "concurrently": false,
2161 + "method": "btree",
2162 + "with": {}
2163 + },
2164 + "asset_stats_trending_idx": {
2165 + "name": "asset_stats_trending_idx",
2166 + "columns": [
2167 + {
2168 + "expression": "trending_score",
2169 + "isExpression": false,
2170 + "asc": true,
2171 + "nulls": "last"
2172 + }
2173 + ],
2174 + "isUnique": false,
2175 + "concurrently": false,
2176 + "method": "btree",
2177 + "with": {}
2178 + },
2179 + "asset_stats_liquidity_idx": {
2180 + "name": "asset_stats_liquidity_idx",
2181 + "columns": [
2182 + {
2183 + "expression": "liquidity_score",
2184 + "isExpression": false,
2185 + "asc": true,
2186 + "nulls": "last"
2187 + }
2188 + ],
2189 + "isUnique": false,
2190 + "concurrently": false,
2191 + "method": "btree",
2192 + "with": {}
2193 + }
2194 + },
2195 + "foreignKeys": {},
2196 + "compositePrimaryKeys": {},
2197 + "uniqueConstraints": {},
2198 + "policies": {},
2199 + "checkConstraints": {},
2200 + "isRLSEnabled": false
2201 + },
2202 + "public.asset_variants": {
2203 + "name": "asset_variants",
2204 + "schema": "",
2205 + "columns": {
2206 + "id": {
2207 + "name": "id",
2208 + "type": "text",
2209 + "primaryKey": true,
2210 + "notNull": true
2211 + },
2212 + "asset_id": {
2213 + "name": "asset_id",
2214 + "type": "text",
2215 + "primaryKey": false,
2216 + "notNull": true
2217 + },
2218 + "variant_key": {
2219 + "name": "variant_key",
2220 + "type": "text",
2221 + "primaryKey": false,
2222 + "notNull": true
2223 + },
2224 + "grader": {
2225 + "name": "grader",
2226 + "type": "text",
2227 + "primaryKey": false,
2228 + "notNull": false
2229 + },
2230 + "grade": {
2231 + "name": "grade",
2232 + "type": "text",
2233 + "primaryKey": false,
2234 + "notNull": false
2235 + },
2236 + "qualifier": {
2237 + "name": "qualifier",
2238 + "type": "text",
2239 + "primaryKey": false,
2240 + "notNull": false
2241 + },
2242 + "condition": {
2243 + "name": "condition",
2244 + "type": "text",
2245 + "primaryKey": false,
2246 + "notNull": false
2247 + },
2248 + "completeness": {
2249 + "name": "completeness",
2250 + "type": "text",
2251 + "primaryKey": false,
2252 + "notNull": false
2253 + },
2254 + "size_label": {
2255 + "name": "size_label",
2256 + "type": "text",
2257 + "primaryKey": false,
2258 + "notNull": false
2259 + },
2260 + "label": {
2261 + "name": "label",
2262 + "type": "text",
2263 + "primaryKey": false,
2264 + "notNull": true
2265 + },
2266 + "is_default": {
2267 + "name": "is_default",
2268 + "type": "boolean",
2269 + "primaryKey": false,
2270 + "notNull": true,
2271 + "default": false
2272 + },
2273 + "created_at": {
2274 + "name": "created_at",
2275 + "type": "timestamp with time zone",
2276 + "primaryKey": false,
2277 + "notNull": true,
2278 + "default": "now()"
2279 + }
2280 + },
2281 + "indexes": {
2282 + "asset_variants_uq": {
2283 + "name": "asset_variants_uq",
2284 + "columns": [
2285 + {
2286 + "expression": "asset_id",
2287 + "isExpression": false,
2288 + "asc": true,
2289 + "nulls": "last"
2290 + },
2291 + {
2292 + "expression": "variant_key",
2293 + "isExpression": false,
2294 + "asc": true,
2295 + "nulls": "last"
2296 + }
2297 + ],
2298 + "isUnique": true,
2299 + "concurrently": false,
2300 + "method": "btree",
2301 + "with": {}
2302 + },
2303 + "asset_variants_asset_idx": {
2304 + "name": "asset_variants_asset_idx",
2305 + "columns": [
2306 + {
2307 + "expression": "asset_id",
2308 + "isExpression": false,
2309 + "asc": true,
2310 + "nulls": "last"
2311 + }
2312 + ],
2313 + "isUnique": false,
2314 + "concurrently": false,
2315 + "method": "btree",
2316 + "with": {}
2317 + }
2318 + },
2319 + "foreignKeys": {},
2320 + "compositePrimaryKeys": {},
2321 + "uniqueConstraints": {},
2322 + "policies": {},
2323 + "checkConstraints": {},
2324 + "isRLSEnabled": false
2325 + },
2326 + "public.assets": {
2327 + "name": "assets",
2328 + "schema": "",
2329 + "columns": {
2330 + "id": {
2331 + "name": "id",
2332 + "type": "text",
2333 + "primaryKey": true,
2334 + "notNull": true
2335 + },
2336 + "slug": {
2337 + "name": "slug",
2338 + "type": "text",
2339 + "primaryKey": false,
2340 + "notNull": true
2341 + },
2342 + "canonical_key": {
2343 + "name": "canonical_key",
2344 + "type": "text",
2345 + "primaryKey": false,
2346 + "notNull": true
2347 + },
2348 + "category_slug": {
2349 + "name": "category_slug",
2350 + "type": "text",
2351 + "primaryKey": false,
2352 + "notNull": true
2353 + },
2354 + "subcategory_slug": {
2355 + "name": "subcategory_slug",
2356 + "type": "text",
2357 + "primaryKey": false,
2358 + "notNull": false
2359 + },
2360 + "family_slug": {
2361 + "name": "family_slug",
2362 + "type": "text",
2363 + "primaryKey": false,
2364 + "notNull": true
2365 + },
2366 + "franchise": {
2367 + "name": "franchise",
2368 + "type": "text",
2369 + "primaryKey": false,
2370 + "notNull": false
2371 + },
2372 + "brand": {
2373 + "name": "brand",
2374 + "type": "text",
2375 + "primaryKey": false,
2376 + "notNull": false
2377 + },
2378 + "series": {
2379 + "name": "series",
2380 + "type": "text",
2381 + "primaryKey": false,
2382 + "notNull": false
2383 + },
2384 + "set_slug": {
2385 + "name": "set_slug",
2386 + "type": "text",
2387 + "primaryKey": false,
2388 + "notNull": false
2389 + },
2390 + "set_name": {
2391 + "name": "set_name",
2392 + "type": "text",
2393 + "primaryKey": false,
2394 + "notNull": false
2395 + },
2396 + "set_code": {
2397 + "name": "set_code",
2398 + "type": "text",
2399 + "primaryKey": false,
2400 + "notNull": false
2401 + },
2402 + "name": {
2403 + "name": "name",
2404 + "type": "text",
2405 + "primaryKey": false,
2406 + "notNull": true
2407 + },
2408 + "title": {
2409 + "name": "title",
2410 + "type": "text",
2411 + "primaryKey": false,
2412 + "notNull": true
2413 + },
2414 + "model": {
2415 + "name": "model",
2416 + "type": "text",
2417 + "primaryKey": false,
2418 + "notNull": false
2419 + },
2420 + "reference": {
2421 + "name": "reference",
2422 + "type": "text",
2423 + "primaryKey": false,
2424 + "notNull": false
2425 + },
2426 + "number": {
2427 + "name": "number",
2428 + "type": "text",
2429 + "primaryKey": false,
2430 + "notNull": false
2431 + },
2432 + "year": {
2433 + "name": "year",
2434 + "type": "integer",
2435 + "primaryKey": false,
2436 + "notNull": false
2437 + },
2438 + "edition": {
2439 + "name": "edition",
2440 + "type": "text",
2441 + "primaryKey": false,
2442 + "notNull": false
2443 + },
2444 + "variant": {
2445 + "name": "variant",
2446 + "type": "text",
2447 + "primaryKey": false,
2448 + "notNull": false
2449 + },
2450 + "language": {
2451 + "name": "language",
2452 + "type": "text",
2453 + "primaryKey": false,
2454 + "notNull": false
2455 + },
2456 + "region": {
2457 + "name": "region",
2458 + "type": "text",
2459 + "primaryKey": false,
2460 + "notNull": false
2461 + },
2462 + "country": {
2463 + "name": "country",
2464 + "type": "text",
2465 + "primaryKey": false,
2466 + "notNull": false
2467 + },
2468 + "material": {
2469 + "name": "material",
2470 + "type": "text",
2471 + "primaryKey": false,
2472 + "notNull": false
2473 + },
2474 + "size": {
2475 + "name": "size",
2476 + "type": "text",
2477 + "primaryKey": false,
2478 + "notNull": false
2479 + },
2480 + "color": {
2481 + "name": "color",
2482 + "type": "text",
2483 + "primaryKey": false,
2484 + "notNull": false
2485 + },
2486 + "rarity": {
2487 + "name": "rarity",
2488 + "type": "text",
2489 + "primaryKey": false,
2490 + "notNull": false
2491 + },
2492 + "production_quantity": {
2493 + "name": "production_quantity",
2494 + "type": "integer",
2495 + "primaryKey": false,
2496 + "notNull": false
2497 + },
2498 + "original_msrp": {
2499 + "name": "original_msrp",
2500 + "type": "numeric(18, 4)",
2501 + "primaryKey": false,
2502 + "notNull": false
2503 + },
2504 + "original_msrp_currency": {
2505 + "name": "original_msrp_currency",
2506 + "type": "text",
2507 + "primaryKey": false,
2508 + "notNull": false
2509 + },
2510 + "release_date": {
2511 + "name": "release_date",
2512 + "type": "text",
2513 + "primaryKey": false,
2514 + "notNull": false
2515 + },
2516 + "description": {
2517 + "name": "description",
2518 + "type": "text",
2519 + "primaryKey": false,
2520 + "notNull": false
2521 + },
2522 + "hero_image_url": {
2523 + "name": "hero_image_url",
2524 + "type": "text",
2525 + "primaryKey": false,
2526 + "notNull": false
2527 + },
2528 + "identifiers": {
2529 + "name": "identifiers",
2530 + "type": "jsonb",
2531 + "primaryKey": false,
2532 + "notNull": true,
2533 + "default": "'{}'::jsonb"
2534 + },
2535 + "metadata": {
2536 + "name": "metadata",
2537 + "type": "jsonb",
2538 + "primaryKey": false,
2539 + "notNull": true,
2540 + "default": "'{}'::jsonb"
2541 + },
2542 + "merged_from": {
2543 + "name": "merged_from",
2544 + "type": "text[]",
2545 + "primaryKey": false,
2546 + "notNull": true,
2547 + "default": "'{}'::text[]"
2548 + },
2549 + "data_quality": {
2550 + "name": "data_quality",
2551 + "type": "real",
2552 + "primaryKey": false,
2553 + "notNull": true,
2554 + "default": 0
2555 + },
2556 + "verified": {
2557 + "name": "verified",
2558 + "type": "boolean",
2559 + "primaryKey": false,
2560 + "notNull": true,
2561 + "default": false
2562 + },
2563 + "search": {
2564 + "name": "search",
2565 + "type": "tsvector",
2566 + "primaryKey": false,
2567 + "notNull": false,
2568 + "generated": {
2569 + "as": "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')",
2570 + "type": "stored"
2571 + }
2572 + },
2573 + "created_at": {
2574 + "name": "created_at",
2575 + "type": "timestamp with time zone",
2576 + "primaryKey": false,
2577 + "notNull": true,
2578 + "default": "now()"
2579 + },
2580 + "updated_at": {
2581 + "name": "updated_at",
2582 + "type": "timestamp with time zone",
2583 + "primaryKey": false,
2584 + "notNull": true,
2585 + "default": "now()"
2586 + }
2587 + },
2588 + "indexes": {
2589 + "assets_canonical_key_uq": {
2590 + "name": "assets_canonical_key_uq",
2591 + "columns": [
2592 + {
2593 + "expression": "canonical_key",
2594 + "isExpression": false,
2595 + "asc": true,
2596 + "nulls": "last"
2597 + }
2598 + ],
2599 + "isUnique": true,
2600 + "concurrently": false,
2601 + "method": "btree",
2602 + "with": {}
2603 + },
2604 + "assets_slug_uq": {
2605 + "name": "assets_slug_uq",
2606 + "columns": [
2607 + {
2608 + "expression": "slug",
2609 + "isExpression": false,
2610 + "asc": true,
2611 + "nulls": "last"
2612 + }
2613 + ],
2614 + "isUnique": true,
2615 + "concurrently": false,
2616 + "method": "btree",
2617 + "with": {}
2618 + },
2619 + "assets_category_idx": {
2620 + "name": "assets_category_idx",
2621 + "columns": [
2622 + {
2623 + "expression": "category_slug",
2624 + "isExpression": false,
2625 + "asc": true,
2626 + "nulls": "last"
2627 + }
2628 + ],
2629 + "isUnique": false,
2630 + "concurrently": false,
2631 + "method": "btree",
2632 + "with": {}
2633 + },
2634 + "assets_family_idx": {
2635 + "name": "assets_family_idx",
2636 + "columns": [
2637 + {
2638 + "expression": "family_slug",
2639 + "isExpression": false,
2640 + "asc": true,
2641 + "nulls": "last"
2642 + }
2643 + ],
2644 + "isUnique": false,
2645 + "concurrently": false,
2646 + "method": "btree",
2647 + "with": {}
2648 + },
2649 + "assets_set_idx": {
2650 + "name": "assets_set_idx",
2651 + "columns": [
2652 + {
2653 + "expression": "set_slug",
2654 + "isExpression": false,
2655 + "asc": true,
2656 + "nulls": "last"
2657 + }
2658 + ],
2659 + "isUnique": false,
2660 + "concurrently": false,
2661 + "method": "btree",
2662 + "with": {}
2663 + },
2664 + "assets_search_gin": {
2665 + "name": "assets_search_gin",
2666 + "columns": [
2667 + {
2668 + "expression": "search",
2669 + "isExpression": false,
2670 + "asc": true,
2671 + "nulls": "last"
2672 + }
2673 + ],
2674 + "isUnique": false,
2675 + "concurrently": false,
2676 + "method": "gin",
2677 + "with": {}
2678 + },
2679 + "assets_title_trgm": {
2680 + "name": "assets_title_trgm",
2681 + "columns": [
2682 + {
2683 + "expression": "\"title\" gin_trgm_ops",
2684 + "asc": true,
2685 + "isExpression": true,
2686 + "nulls": "last"
2687 + }
2688 + ],
2689 + "isUnique": false,
2690 + "concurrently": false,
2691 + "method": "gin",
2692 + "with": {}
2693 + },
2694 + "assets_identifiers_gin": {
2695 + "name": "assets_identifiers_gin",
2696 + "columns": [
2697 + {
2698 + "expression": "identifiers",
2699 + "isExpression": false,
2700 + "asc": true,
2701 + "nulls": "last"
2702 + }
2703 + ],
2704 + "isUnique": false,
2705 + "concurrently": false,
2706 + "method": "gin",
2707 + "with": {}
2708 + }
2709 + },
2710 + "foreignKeys": {},
2711 + "compositePrimaryKeys": {},
2712 + "uniqueConstraints": {},
2713 + "policies": {},
2714 + "checkConstraints": {},
2715 + "isRLSEnabled": false
2716 + },
2717 + "public.grade_premiums": {
2718 + "name": "grade_premiums",
2719 + "schema": "",
2720 + "columns": {
2721 + "id": {
2722 + "name": "id",
2723 + "type": "text",
2724 + "primaryKey": true,
2725 + "notNull": true
2726 + },
2727 + "category_slug": {
2728 + "name": "category_slug",
2729 + "type": "text",
2730 + "primaryKey": false,
2731 + "notNull": true
2732 + },
2733 + "grader": {
2734 + "name": "grader",
2735 + "type": "text",
2736 + "primaryKey": false,
2737 + "notNull": true
2738 + },
2739 + "grade": {
2740 + "name": "grade",
2741 + "type": "text",
2742 + "primaryKey": false,
2743 + "notNull": true
2744 + },
2745 + "market_multiplier": {
2746 + "name": "market_multiplier",
2747 + "type": "real",
2748 + "primaryKey": false,
2749 + "notNull": true
2750 + },
2751 + "sample_size": {
2752 + "name": "sample_size",
2753 + "type": "integer",
2754 + "primaryKey": false,
2755 + "notNull": true
2756 + },
2757 + "computed_at": {
2758 + "name": "computed_at",
2759 + "type": "timestamp with time zone",
2760 + "primaryKey": false,
2761 + "notNull": true
2762 + }
2763 + },
2764 + "indexes": {
2765 + "grade_premiums_uq": {
2766 + "name": "grade_premiums_uq",
2767 + "columns": [
2768 + {
2769 + "expression": "category_slug",
2770 + "isExpression": false,
2771 + "asc": true,
2772 + "nulls": "last"
2773 + },
2774 + {
2775 + "expression": "grader",
2776 + "isExpression": false,
2777 + "asc": true,
2778 + "nulls": "last"
2779 + },
2780 + {
2781 + "expression": "grade",
2782 + "isExpression": false,
2783 + "asc": true,
2784 + "nulls": "last"
2785 + }
2786 + ],
2787 + "isUnique": true,
2788 + "concurrently": false,
2789 + "method": "btree",
2790 + "with": {}
2791 + }
2792 + },
2793 + "foreignKeys": {},
2794 + "compositePrimaryKeys": {},
2795 + "uniqueConstraints": {},
2796 + "policies": {},
2797 + "checkConstraints": {},
2798 + "isRLSEnabled": false
2799 + },
2800 + "public.images": {
2801 + "name": "images",
2802 + "schema": "",
2803 + "columns": {
2804 + "id": {
2805 + "name": "id",
2806 + "type": "text",
2807 + "primaryKey": true,
2808 + "notNull": true
2809 + },
2810 + "asset_id": {
2811 + "name": "asset_id",
2812 + "type": "text",
2813 + "primaryKey": false,
2814 + "notNull": false
2815 + },
2816 + "listing_id": {
2817 + "name": "listing_id",
2818 + "type": "text",
2819 + "primaryKey": false,
2820 + "notNull": false
2821 + },
2822 + "sale_id": {
2823 + "name": "sale_id",
2824 + "type": "text",
2825 + "primaryKey": false,
2826 + "notNull": false
2827 + },
2828 + "source_id": {
2829 + "name": "source_id",
2830 + "type": "text",
2831 + "primaryKey": false,
2832 + "notNull": false
2833 + },
2834 + "url": {
2835 + "name": "url",
2836 + "type": "text",
2837 + "primaryKey": false,
2838 + "notNull": true
2839 + },
2840 + "role": {
2841 + "name": "role",
2842 + "type": "text",
2843 + "primaryKey": false,
2844 + "notNull": true,
2845 + "default": "'gallery'"
2846 + },
2847 + "width": {
2848 + "name": "width",
2849 + "type": "integer",
2850 + "primaryKey": false,
2851 + "notNull": false
2852 + },
2853 + "height": {
2854 + "name": "height",
2855 + "type": "integer",
2856 + "primaryKey": false,
2857 + "notNull": false
2858 + },
2859 + "phash": {
2860 + "name": "phash",
2861 + "type": "text",
2862 + "primaryKey": false,
2863 + "notNull": false
2864 + },
2865 + "embedding": {
2866 + "name": "embedding",
2867 + "type": "vector(512)",
2868 + "primaryKey": false,
2869 + "notNull": false
2870 + },
2871 + "attribution": {
2872 + "name": "attribution",
2873 + "type": "text",
2874 + "primaryKey": false,
2875 + "notNull": false
2876 + },
2877 + "status": {
2878 + "name": "status",
2879 + "type": "text",
2880 + "primaryKey": false,
2881 + "notNull": true,
2882 + "default": "'unchecked'"
2883 + },
2884 + "checked_at": {
2885 + "name": "checked_at",
2886 + "type": "timestamp with time zone",
2887 + "primaryKey": false,
2888 + "notNull": false
2889 + },
2890 + "bytes": {
2891 + "name": "bytes",
2892 + "type": "integer",
2893 + "primaryKey": false,
2894 + "notNull": false
2895 + },
2896 + "content_type": {
2897 + "name": "content_type",
2898 + "type": "text",
2899 + "primaryKey": false,
2900 + "notNull": false
2901 + },
2902 + "cache_key": {
2903 + "name": "cache_key",
2904 + "type": "text",
2905 + "primaryKey": false,
2906 + "notNull": false
2907 + },
2908 + "error": {
2909 + "name": "error",
2910 + "type": "text",
2911 + "primaryKey": false,
2912 + "notNull": false
2913 + },
2914 + "created_at": {
2915 + "name": "created_at",
2916 + "type": "timestamp with time zone",
2917 + "primaryKey": false,
2918 + "notNull": true,
2919 + "default": "now()"
2920 + }
2921 + },
2922 + "indexes": {
2923 + "images_asset_idx": {
2924 + "name": "images_asset_idx",
2925 + "columns": [
2926 + {
2927 + "expression": "asset_id",
2928 + "isExpression": false,
2929 + "asc": true,
2930 + "nulls": "last"
2931 + }
2932 + ],
2933 + "isUnique": false,
2934 + "concurrently": false,
2935 + "method": "btree",
2936 + "with": {}
2937 + },
2938 + "images_phash_idx": {
2939 + "name": "images_phash_idx",
2940 + "columns": [
2941 + {
2942 + "expression": "phash",
2943 + "isExpression": false,
2944 + "asc": true,
2945 + "nulls": "last"
2946 + }
2947 + ],
2948 + "isUnique": false,
2949 + "concurrently": false,
2950 + "method": "btree",
2951 + "with": {}
2952 + },
2953 + "images_url_uq": {
2954 + "name": "images_url_uq",
2955 + "columns": [
2956 + {
2957 + "expression": "url",
2958 + "isExpression": false,
2959 + "asc": true,
2960 + "nulls": "last"
2961 + }
2962 + ],
2963 + "isUnique": true,
2964 + "concurrently": false,
2965 + "method": "btree",
2966 + "with": {}
2967 + },
2968 + "images_cache_key_idx": {
2969 + "name": "images_cache_key_idx",
2970 + "columns": [
2971 + {
2972 + "expression": "cache_key",
2973 + "isExpression": false,
2974 + "asc": true,
2975 + "nulls": "last"
2976 + }
2977 + ],
2978 + "isUnique": false,
2979 + "concurrently": false,
2980 + "method": "btree",
2981 + "with": {}
2982 + },
2983 + "images_status_idx": {
2984 + "name": "images_status_idx",
2985 + "columns": [
2986 + {
2987 + "expression": "status",
2988 + "isExpression": false,
2989 + "asc": true,
2990 + "nulls": "last"
2991 + },
2992 + {
2993 + "expression": "checked_at",
2994 + "isExpression": false,
2995 + "asc": true,
2996 + "nulls": "last"
2997 + }
2998 + ],
2999 + "isUnique": false,
3000 + "concurrently": false,
3001 + "method": "btree",
3002 + "with": {}
3003 + }
3004 + },
3005 + "foreignKeys": {},
3006 + "compositePrimaryKeys": {},
3007 + "uniqueConstraints": {},
3008 + "policies": {},
3009 + "checkConstraints": {},
3010 + "isRLSEnabled": false
3011 + },
3012 + "public.population_reports": {
3013 + "name": "population_reports",
3014 + "schema": "",
3015 + "columns": {
3016 + "id": {
3017 + "name": "id",
3018 + "type": "text",
3019 + "primaryKey": true,
3020 + "notNull": true
3021 + },
3022 + "asset_id": {
3023 + "name": "asset_id",
3024 + "type": "text",
3025 + "primaryKey": false,
3026 + "notNull": true
3027 + },
3028 + "grader": {
3029 + "name": "grader",
3030 + "type": "text",
3031 + "primaryKey": false,
3032 + "notNull": true
3033 + },
3034 + "source_id": {
3035 + "name": "source_id",
3036 + "type": "text",
3037 + "primaryKey": false,
3038 + "notNull": true
3039 + },
3040 + "source_url": {
3041 + "name": "source_url",
3042 + "type": "text",
3043 + "primaryKey": false,
3044 + "notNull": false
3045 + },
3046 + "report_date": {
3047 + "name": "report_date",
3048 + "type": "text",
3049 + "primaryKey": false,
3050 + "notNull": true
3051 + },
3052 + "total": {
3053 + "name": "total",
3054 + "type": "integer",
3055 + "primaryKey": false,
3056 + "notNull": true
3057 + },
3058 + "by_grade": {
3059 + "name": "by_grade",
3060 + "type": "jsonb",
3061 + "primaryKey": false,
3062 + "notNull": true
3063 + },
3064 + "created_at": {
3065 + "name": "created_at",
3066 + "type": "timestamp with time zone",
3067 + "primaryKey": false,
3068 + "notNull": true,
3069 + "default": "now()"
3070 + }
3071 + },
3072 + "indexes": {
3073 + "population_reports_uq": {
3074 + "name": "population_reports_uq",
3075 + "columns": [
3076 + {
3077 + "expression": "asset_id",
3078 + "isExpression": false,
3079 + "asc": true,
3080 + "nulls": "last"
3081 + },
3082 + {
3083 + "expression": "grader",
3084 + "isExpression": false,
3085 + "asc": true,
3086 + "nulls": "last"
3087 + },
3088 + {
3089 + "expression": "report_date",
3090 + "isExpression": false,
3091 + "asc": true,
3092 + "nulls": "last"
3093 + }
3094 + ],
3095 + "isUnique": true,
3096 + "concurrently": false,
3097 + "method": "btree",
3098 + "with": {}
3099 + }
3100 + },
3101 + "foreignKeys": {},
3102 + "compositePrimaryKeys": {},
3103 + "uniqueConstraints": {},
3104 + "policies": {},
3105 + "checkConstraints": {},
3106 + "isRLSEnabled": false
3107 + },
3108 + "public.variant_stats": {
3109 + "name": "variant_stats",
3110 + "schema": "",
3111 + "columns": {
3112 + "variant_id": {
3113 + "name": "variant_id",
3114 + "type": "text",
3115 + "primaryKey": true,
3116 + "notNull": true
3117 + },
3118 + "asset_id": {
3119 + "name": "asset_id",
3120 + "type": "text",
3121 + "primaryKey": false,
3122 + "notNull": true
3123 + },
3124 + "riv_usd": {
3125 + "name": "riv_usd",
3126 + "type": "numeric(18, 4)",
3127 + "primaryKey": false,
3128 + "notNull": false
3129 + },
3130 + "riv_low_usd": {
3131 + "name": "riv_low_usd",
3132 + "type": "numeric(18, 4)",
3133 + "primaryKey": false,
3134 + "notNull": false
3135 + },
3136 + "riv_high_usd": {
3137 + "name": "riv_high_usd",
3138 + "type": "numeric(18, 4)",
3139 + "primaryKey": false,
3140 + "notNull": false
3141 + },
3142 + "riv_confidence": {
3143 + "name": "riv_confidence",
3144 + "type": "numeric(8, 6)",
3145 + "primaryKey": false,
3146 + "notNull": false
3147 + },
3148 + "riv_sample_size": {
3149 + "name": "riv_sample_size",
3150 + "type": "integer",
3151 + "primaryKey": false,
3152 + "notNull": true,
3153 + "default": 0
3154 + },
3155 + "latest_sale_usd": {
3156 + "name": "latest_sale_usd",
3157 + "type": "numeric(18, 4)",
3158 + "primaryKey": false,
3159 + "notNull": false
3160 + },
3161 + "latest_sale_at": {
3162 + "name": "latest_sale_at",
3163 + "type": "timestamp with time zone",
3164 + "primaryKey": false,
3165 + "notNull": false
3166 + },
3167 + "change_30d": {
3168 + "name": "change_30d",
3169 + "type": "numeric(8, 6)",
3170 + "primaryKey": false,
3171 + "notNull": false
3172 + },
3173 + "change_1y": {
3174 + "name": "change_1y",
3175 + "type": "numeric(8, 6)",
3176 + "primaryKey": false,
3177 + "notNull": false
3178 + },
3179 + "sales_count": {
3180 + "name": "sales_count",
3181 + "type": "integer",
3182 + "primaryKey": false,
3183 + "notNull": true,
3184 + "default": 0
3185 + },
3186 + "sales_30d": {
3187 + "name": "sales_30d",
3188 + "type": "integer",
3189 + "primaryKey": false,
3190 + "notNull": true,
3191 + "default": 0
3192 + },
3193 + "active_listings": {
3194 + "name": "active_listings",
3195 + "type": "integer",
3196 + "primaryKey": false,
3197 + "notNull": true,
3198 + "default": 0
3199 + },
3200 + "min_ask_usd": {
3201 + "name": "min_ask_usd",
3202 + "type": "numeric(18, 4)",
3203 + "primaryKey": false,
3204 + "notNull": false
3205 + },
3206 + "liquidity_score": {
3207 + "name": "liquidity_score",
3208 + "type": "real",
3209 + "primaryKey": false,
3210 + "notNull": false
3211 + },
3212 + "updated_at": {
3213 + "name": "updated_at",
3214 + "type": "timestamp with time zone",
3215 + "primaryKey": false,
3216 + "notNull": true,
3217 + "default": "now()"
3218 + }
3219 + },
3220 + "indexes": {},
3221 + "foreignKeys": {},
3222 + "compositePrimaryKeys": {},
3223 + "uniqueConstraints": {},
3224 + "policies": {},
3225 + "checkConstraints": {},
3226 + "isRLSEnabled": false
3227 + },
3228 + "public.auction_lots": {
3229 + "name": "auction_lots",
3230 + "schema": "",
3231 + "columns": {
3232 + "id": {
3233 + "name": "id",
3234 + "type": "text",
3235 + "primaryKey": true,
3236 + "notNull": true
3237 + },
3238 + "auction_id": {
3239 + "name": "auction_id",
3240 + "type": "text",
3241 + "primaryKey": false,
3242 + "notNull": true
3243 + },
3244 + "asset_id": {
3245 + "name": "asset_id",
3246 + "type": "text",
3247 + "primaryKey": false,
3248 + "notNull": false
3249 + },
3250 + "variant_id": {
3251 + "name": "variant_id",
3252 + "type": "text",
3253 + "primaryKey": false,
3254 + "notNull": false
3255 + },
3256 + "source_id": {
3257 + "name": "source_id",
3258 + "type": "text",
3259 + "primaryKey": false,
3260 + "notNull": true
3261 + },
3262 + "lot_number": {
3263 + "name": "lot_number",
3264 + "type": "text",
3265 + "primaryKey": false,
3266 + "notNull": false
3267 + },
3268 + "title": {
3269 + "name": "title",
3270 + "type": "text",
3271 + "primaryKey": false,
3272 + "notNull": true
3273 + },
3274 + "url": {
3275 + "name": "url",
3276 + "type": "text",
3277 + "primaryKey": false,
3278 + "notNull": true
3279 + },
3280 + "estimate_low": {
3281 + "name": "estimate_low",
3282 + "type": "numeric(18, 4)",
3283 + "primaryKey": false,
3284 + "notNull": false
3285 + },
3286 + "estimate_high": {
3287 + "name": "estimate_high",
3288 + "type": "numeric(18, 4)",
3289 + "primaryKey": false,
3290 + "notNull": false
3291 + },
3292 + "current_bid": {
3293 + "name": "current_bid",
3294 + "type": "numeric(18, 4)",
3295 + "primaryKey": false,
3296 + "notNull": false
3297 + },
3298 + "hammer_price": {
3299 + "name": "hammer_price",
3300 + "type": "numeric(18, 4)",
3301 + "primaryKey": false,
3302 + "notNull": false
3303 + },
3304 + "currency": {
3305 + "name": "currency",
3306 + "type": "text",
3307 + "primaryKey": false,
3308 + "notNull": false
3309 + },
3310 + "bid_count": {
3311 + "name": "bid_count",
3312 + "type": "integer",
3313 + "primaryKey": false,
3314 + "notNull": false
3315 + },
3316 + "starts_at": {
3317 + "name": "starts_at",
3318 + "type": "timestamp with time zone",
3319 + "primaryKey": false,
3320 + "notNull": false
3321 + },
3322 + "ends_at": {
3323 + "name": "ends_at",
3324 + "type": "timestamp with time zone",
3325 + "primaryKey": false,
3326 + "notNull": false
3327 + },
3328 + "status": {
3329 + "name": "status",
3330 + "type": "text",
3331 + "primaryKey": false,
3332 + "notNull": true,
3333 + "default": "'upcoming'"
3334 + },
3335 + "image_urls": {
3336 + "name": "image_urls",
3337 + "type": "jsonb",
3338 + "primaryKey": false,
3339 + "notNull": true,
3340 + "default": "'[]'::jsonb"
3341 + },
3342 + "grader": {
3343 + "name": "grader",
3344 + "type": "text",
3345 + "primaryKey": false,
3346 + "notNull": false
3347 + },
3348 + "grade": {
3349 + "name": "grade",
3350 + "type": "text",
3351 + "primaryKey": false,
3352 + "notNull": false
3353 + },
3354 + "created_at": {
3355 + "name": "created_at",
3356 + "type": "timestamp with time zone",
3357 + "primaryKey": false,
3358 + "notNull": true,
3359 + "default": "now()"
3360 + },
3361 + "updated_at": {
3362 + "name": "updated_at",
3363 + "type": "timestamp with time zone",
3364 + "primaryKey": false,
3365 + "notNull": true,
3366 + "default": "now()"
3367 + }
3368 + },
3369 + "indexes": {
3370 + "auction_lots_url_uq": {
3371 + "name": "auction_lots_url_uq",
3372 + "columns": [
3373 + {
3374 + "expression": "url",
3375 + "isExpression": false,
3376 + "asc": true,
3377 + "nulls": "last"
3378 + }
3379 + ],
3380 + "isUnique": true,
3381 + "concurrently": false,
3382 + "method": "btree",
3383 + "with": {}
3384 + },
3385 + "auction_lots_auction_idx": {
3386 + "name": "auction_lots_auction_idx",
3387 + "columns": [
3388 + {
3389 + "expression": "auction_id",
3390 + "isExpression": false,
3391 + "asc": true,
3392 + "nulls": "last"
3393 + }
3394 + ],
3395 + "isUnique": false,
3396 + "concurrently": false,
3397 + "method": "btree",
3398 + "with": {}
3399 + },
3400 + "auction_lots_asset_idx": {
3401 + "name": "auction_lots_asset_idx",
3402 + "columns": [
3403 + {
3404 + "expression": "asset_id",
3405 + "isExpression": false,
3406 + "asc": true,
3407 + "nulls": "last"
3408 + }
3409 + ],
3410 + "isUnique": false,
3411 + "concurrently": false,
3412 + "method": "btree",
3413 + "with": {}
3414 + },
3415 + "auction_lots_ends_idx": {
3416 + "name": "auction_lots_ends_idx",
3417 + "columns": [
3418 + {
3419 + "expression": "ends_at",
3420 + "isExpression": false,
3421 + "asc": true,
3422 + "nulls": "last"
3423 + }
3424 + ],
3425 + "isUnique": false,
3426 + "concurrently": false,
3427 + "method": "btree",
3428 + "with": {}
3429 + }
3430 + },
3431 + "foreignKeys": {},
3432 + "compositePrimaryKeys": {},
3433 + "uniqueConstraints": {},
3434 + "policies": {},
3435 + "checkConstraints": {},
3436 + "isRLSEnabled": false
3437 + },
3438 + "public.auctions": {
3439 + "name": "auctions",
3440 + "schema": "",
3441 + "columns": {
3442 + "id": {
3443 + "name": "id",
3444 + "type": "text",
3445 + "primaryKey": true,
3446 + "notNull": true
3447 + },
3448 + "source_id": {
3449 + "name": "source_id",
3450 + "type": "text",
3451 + "primaryKey": false,
3452 + "notNull": true
3453 + },
3454 + "auction_house": {
3455 + "name": "auction_house",
3456 + "type": "text",
3457 + "primaryKey": false,
3458 + "notNull": true
3459 + },
3460 + "name": {
3461 + "name": "name",
3462 + "type": "text",
3463 + "primaryKey": false,
3464 + "notNull": true
3465 + },
3466 + "url": {
3467 + "name": "url",
3468 + "type": "text",
3469 + "primaryKey": false,
3470 + "notNull": true
3471 + },
3472 + "starts_at": {
3473 + "name": "starts_at",
3474 + "type": "timestamp with time zone",
3475 + "primaryKey": false,
3476 + "notNull": false
3477 + },
3478 + "ends_at": {
3479 + "name": "ends_at",
3480 + "type": "timestamp with time zone",
3481 + "primaryKey": false,
3482 + "notNull": false
3483 + },
3484 + "location": {
3485 + "name": "location",
3486 + "type": "text",
3487 + "primaryKey": false,
3488 + "notNull": false
3489 + },
3490 + "category_slugs": {
3491 + "name": "category_slugs",
3492 + "type": "text[]",
3493 + "primaryKey": false,
3494 + "notNull": true,
3495 + "default": "'{}'::text[]"
3496 + },
3497 + "lot_count": {
3498 + "name": "lot_count",
3499 + "type": "integer",
3500 + "primaryKey": false,
3501 + "notNull": false
3502 + },
3503 + "status": {
3504 + "name": "status",
3505 + "type": "text",
3506 + "primaryKey": false,
3507 + "notNull": true,
3508 + "default": "'upcoming'"
3509 + },
3510 + "currency": {
3511 + "name": "currency",
3512 + "type": "text",
3513 + "primaryKey": false,
3514 + "notNull": false
3515 + },
3516 + "created_at": {
3517 + "name": "created_at",
3518 + "type": "timestamp with time zone",
3519 + "primaryKey": false,
3520 + "notNull": true,
3521 + "default": "now()"
3522 + },
3523 + "updated_at": {
3524 + "name": "updated_at",
3525 + "type": "timestamp with time zone",
3526 + "primaryKey": false,
3527 + "notNull": true,
3528 + "default": "now()"
3529 + }
3530 + },
3531 + "indexes": {
3532 + "auctions_url_uq": {
3533 + "name": "auctions_url_uq",
3534 + "columns": [
3535 + {
3536 + "expression": "url",
3537 + "isExpression": false,
3538 + "asc": true,
3539 + "nulls": "last"
3540 + }
3541 + ],
3542 + "isUnique": true,
3543 + "concurrently": false,
3544 + "method": "btree",
3545 + "with": {}
3546 + },
3547 + "auctions_ends_idx": {
3548 + "name": "auctions_ends_idx",
3549 + "columns": [
3550 + {
3551 + "expression": "ends_at",
3552 + "isExpression": false,
3553 + "asc": true,
3554 + "nulls": "last"
3555 + }
3556 + ],
3557 + "isUnique": false,
3558 + "concurrently": false,
3559 + "method": "btree",
3560 + "with": {}
3561 + }
3562 + },
3563 + "foreignKeys": {},
3564 + "compositePrimaryKeys": {},
3565 + "uniqueConstraints": {},
3566 + "policies": {},
3567 + "checkConstraints": {},
3568 + "isRLSEnabled": false
3569 + },
3570 + "public.cross_listing_groups": {
3571 + "name": "cross_listing_groups",
3572 + "schema": "",
3573 + "columns": {
3574 + "id": {
3575 + "name": "id",
3576 + "type": "text",
3577 + "primaryKey": true,
3578 + "notNull": true
3579 + },
3580 + "asset_id": {
3581 + "name": "asset_id",
3582 + "type": "text",
3583 + "primaryKey": false,
3584 + "notNull": false
3585 + },
3586 + "signals": {
3587 + "name": "signals",
3588 + "type": "jsonb",
3589 + "primaryKey": false,
3590 + "notNull": true,
3591 + "default": "'{}'::jsonb"
3592 + },
3593 + "created_at": {
3594 + "name": "created_at",
3595 + "type": "timestamp with time zone",
3596 + "primaryKey": false,
3597 + "notNull": true,
3598 + "default": "now()"
3599 + }
3600 + },
3601 + "indexes": {},
3602 + "foreignKeys": {},
3603 + "compositePrimaryKeys": {},
3604 + "uniqueConstraints": {},
3605 + "policies": {},
3606 + "checkConstraints": {},
3607 + "isRLSEnabled": false
3608 + },
3609 + "public.fx_rates": {
3610 + "name": "fx_rates",
3611 + "schema": "",
3612 + "columns": {
3613 + "date": {
3614 + "name": "date",
3615 + "type": "date",
3616 + "primaryKey": false,
3617 + "notNull": true
3618 + },
3619 + "base": {
3620 + "name": "base",
3621 + "type": "text",
3622 + "primaryKey": false,
3623 + "notNull": true
3624 + },
3625 + "quote": {
3626 + "name": "quote",
3627 + "type": "text",
3628 + "primaryKey": false,
3629 + "notNull": true
3630 + },
3631 + "rate": {
3632 + "name": "rate",
3633 + "type": "real",
3634 + "primaryKey": false,
3635 + "notNull": true
3636 + },
3637 + "source": {
3638 + "name": "source",
3639 + "type": "text",
3640 + "primaryKey": false,
3641 + "notNull": true,
3642 + "default": "'ecb'"
3643 + }
3644 + },
3645 + "indexes": {
3646 + "fx_rates_uq": {
3647 + "name": "fx_rates_uq",
3648 + "columns": [
3649 + {
3650 + "expression": "date",
3651 + "isExpression": false,
3652 + "asc": true,
3653 + "nulls": "last"
3654 + },
3655 + {
3656 + "expression": "base",
3657 + "isExpression": false,
3658 + "asc": true,
3659 + "nulls": "last"
3660 + },
3661 + {
3662 + "expression": "quote",
3663 + "isExpression": false,
3664 + "asc": true,
3665 + "nulls": "last"
3666 + }
3667 + ],
3668 + "isUnique": true,
3669 + "concurrently": false,
3670 + "method": "btree",
3671 + "with": {}
3672 + }
3673 + },
3674 + "foreignKeys": {},
3675 + "compositePrimaryKeys": {},
3676 + "uniqueConstraints": {},
3677 + "policies": {},
3678 + "checkConstraints": {},
3679 + "isRLSEnabled": false
3680 + },
3681 + "public.listing_events": {
3682 + "name": "listing_events",
3683 + "schema": "",
3684 + "columns": {
3685 + "id": {
3686 + "name": "id",
3687 + "type": "text",
3688 + "primaryKey": true,
3689 + "notNull": true
3690 + },
3691 + "listing_id": {
3692 + "name": "listing_id",
3693 + "type": "text",
3694 + "primaryKey": false,
3695 + "notNull": true
3696 + },
3697 + "event_type": {
3698 + "name": "event_type",
3699 + "type": "text",
3700 + "primaryKey": false,
3701 + "notNull": true
3702 + },
3703 + "old_price": {
3704 + "name": "old_price",
3705 + "type": "numeric(18, 4)",
3706 + "primaryKey": false,
3707 + "notNull": false
3708 + },
3709 + "new_price": {
3710 + "name": "new_price",
3711 + "type": "numeric(18, 4)",
3712 + "primaryKey": false,
3713 + "notNull": false
3714 + },
3715 + "currency": {
3716 + "name": "currency",
3717 + "type": "text",
3718 + "primaryKey": false,
3719 + "notNull": false
3720 + },
3721 + "occurred_at": {
3722 + "name": "occurred_at",
3723 + "type": "timestamp with time zone",
3724 + "primaryKey": false,
3725 + "notNull": true
3726 + }
3727 + },
3728 + "indexes": {
3729 + "listing_events_listing_idx": {
3730 + "name": "listing_events_listing_idx",
3731 + "columns": [
3732 + {
3733 + "expression": "listing_id",
3734 + "isExpression": false,
3735 + "asc": true,
3736 + "nulls": "last"
3737 + },
3738 + {
3739 + "expression": "occurred_at",
3740 + "isExpression": false,
3741 + "asc": true,
3742 + "nulls": "last"
3743 + }
3744 + ],
3745 + "isUnique": false,
3746 + "concurrently": false,
3747 + "method": "btree",
3748 + "with": {}
3749 + }
3750 + },
3751 + "foreignKeys": {},
3752 + "compositePrimaryKeys": {},
3753 + "uniqueConstraints": {},
3754 + "policies": {},
3755 + "checkConstraints": {},
3756 + "isRLSEnabled": false
3757 + },
3758 + "public.listings": {
3759 + "name": "listings",
3760 + "schema": "",
3761 + "columns": {
3762 + "id": {
3763 + "name": "id",
3764 + "type": "text",
3765 + "primaryKey": true,
3766 + "notNull": true
3767 + },
3768 + "asset_id": {
3769 + "name": "asset_id",
3770 + "type": "text",
3771 + "primaryKey": false,
3772 + "notNull": true
3773 + },
3774 + "variant_id": {
3775 + "name": "variant_id",
3776 + "type": "text",
3777 + "primaryKey": false,
3778 + "notNull": false
3779 + },
3780 + "source_id": {
3781 + "name": "source_id",
3782 + "type": "text",
3783 + "primaryKey": false,
3784 + "notNull": true
3785 + },
3786 + "connector_id": {
3787 + "name": "connector_id",
3788 + "type": "text",
3789 + "primaryKey": false,
3790 + "notNull": true
3791 + },
3792 + "raw_record_id": {
3793 + "name": "raw_record_id",
3794 + "type": "text",
3795 + "primaryKey": false,
3796 + "notNull": false
3797 + },
3798 + "source_url": {
3799 + "name": "source_url",
3800 + "type": "text",
3801 + "primaryKey": false,
3802 + "notNull": true
3803 + },
3804 + "external_id": {
3805 + "name": "external_id",
3806 + "type": "text",
3807 + "primaryKey": false,
3808 + "notNull": true
3809 + },
3810 + "listing_type": {
3811 + "name": "listing_type",
3812 + "type": "text",
3813 + "primaryKey": false,
3814 + "notNull": true,
3815 + "default": "'unknown'"
3816 + },
3817 + "price": {
3818 + "name": "price",
3819 + "type": "numeric(18, 4)",
3820 + "primaryKey": false,
3821 + "notNull": false
3822 + },
3823 + "currency": {
3824 + "name": "currency",
3825 + "type": "text",
3826 + "primaryKey": false,
3827 + "notNull": false
3828 + },
3829 + "price_usd": {
3830 + "name": "price_usd",
3831 + "type": "numeric(18, 4)",
3832 + "primaryKey": false,
3833 + "notNull": false
3834 + },
3835 + "seller": {
3836 + "name": "seller",
3837 + "type": "text",
3838 + "primaryKey": false,
3839 + "notNull": false
3840 + },
3841 + "seller_reputation": {
3842 + "name": "seller_reputation",
3843 + "type": "text",
3844 + "primaryKey": false,
3845 + "notNull": false
3846 + },
3847 + "location": {
3848 + "name": "location",
3849 + "type": "text",
3850 + "primaryKey": false,
3851 + "notNull": false
3852 + },
3853 + "shipping_cost": {
3854 + "name": "shipping_cost",
3855 + "type": "numeric(18, 4)",
3856 + "primaryKey": false,
3857 + "notNull": false
3858 + },
3859 + "quantity": {
3860 + "name": "quantity",
3861 + "type": "integer",
3862 + "primaryKey": false,
3863 + "notNull": false
3864 + },
3865 + "condition": {
3866 + "name": "condition",
3867 + "type": "text",
3868 + "primaryKey": false,
3869 + "notNull": false
3870 + },
3871 + "grader": {
3872 + "name": "grader",
3873 + "type": "text",
3874 + "primaryKey": false,
3875 + "notNull": false
3876 + },
3877 + "grade": {
3878 + "name": "grade",
3879 + "type": "text",
3880 + "primaryKey": false,
3881 + "notNull": false
3882 + },
3883 + "certification_number": {
3884 + "name": "certification_number",
3885 + "type": "text",
3886 + "primaryKey": false,
3887 + "notNull": false
3888 + },
3889 + "image_urls": {
3890 + "name": "image_urls",
3891 + "type": "jsonb",
3892 + "primaryKey": false,
3893 + "notNull": true,
3894 + "default": "'[]'::jsonb"
3895 + },
3896 + "raw_title": {
3897 + "name": "raw_title",
3898 + "type": "text",
3899 + "primaryKey": false,
3900 + "notNull": true
3901 + },
3902 + "description": {
3903 + "name": "description",
3904 + "type": "text",
3905 + "primaryKey": false,
3906 + "notNull": false
3907 + },
3908 + "listed_at": {
3909 + "name": "listed_at",
3910 + "type": "timestamp with time zone",
3911 + "primaryKey": false,
3912 + "notNull": false
3913 + },
3914 + "ends_at": {
3915 + "name": "ends_at",
3916 + "type": "timestamp with time zone",
3917 + "primaryKey": false,
3918 + "notNull": false
3919 + },
3920 + "availability": {
3921 + "name": "availability",
3922 + "type": "text",
3923 + "primaryKey": false,
3924 + "notNull": true,
3925 + "default": "'available'"
3926 + },
3927 + "bid_count": {
3928 + "name": "bid_count",
3929 + "type": "integer",
3930 + "primaryKey": false,
3931 + "notNull": false
3932 + },
3933 + "first_seen_at": {
3934 + "name": "first_seen_at",
3935 + "type": "timestamp with time zone",
3936 + "primaryKey": false,
3937 + "notNull": true
3938 + },
3939 + "last_seen_at": {
3940 + "name": "last_seen_at",
3941 + "type": "timestamp with time zone",
3942 + "primaryKey": false,
3943 + "notNull": true
3944 + },
3945 + "price_changed_at": {
3946 + "name": "price_changed_at",
3947 + "type": "timestamp with time zone",
3948 + "primaryKey": false,
3949 + "notNull": false
3950 + },
3951 + "cross_listing_group_id": {
3952 + "name": "cross_listing_group_id",
3953 + "type": "text",
3954 + "primaryKey": false,
3955 + "notNull": false
3956 + },
3957 + "confidence": {
3958 + "name": "confidence",
3959 + "type": "numeric(8, 6)",
3960 + "primaryKey": false,
3961 + "notNull": true,
3962 + "default": 0.8
3963 + },
3964 + "data_quality": {
3965 + "name": "data_quality",
3966 + "type": "real",
3967 + "primaryKey": false,
3968 + "notNull": true,
3969 + "default": 0
3970 + },
3971 + "flags": {
3972 + "name": "flags",
3973 + "type": "text[]",
3974 + "primaryKey": false,
3975 + "notNull": true,
3976 + "default": "'{}'::text[]"
3977 + },
3978 + "discount_to_riv": {
3979 + "name": "discount_to_riv",
3980 + "type": "numeric(8, 6)",
3981 + "primaryKey": false,
3982 + "notNull": false
3983 + },
3984 + "created_at": {
3985 + "name": "created_at",
3986 + "type": "timestamp with time zone",
3987 + "primaryKey": false,
3988 + "notNull": true,
3989 + "default": "now()"
3990 + },
3991 + "updated_at": {
3992 + "name": "updated_at",
3993 + "type": "timestamp with time zone",
3994 + "primaryKey": false,
3995 + "notNull": true,
3996 + "default": "now()"
3997 + }
3998 + },
3999 + "indexes": {
4000 + "listings_source_external_uq": {
4001 + "name": "listings_source_external_uq",
4002 + "columns": [
4003 + {
4004 + "expression": "source_id",
4005 + "isExpression": false,
4006 + "asc": true,
4007 + "nulls": "last"
4008 + },
4009 + {
4010 + "expression": "external_id",
4011 + "isExpression": false,
4012 + "asc": true,
4013 + "nulls": "last"
4014 + }
4015 + ],
4016 + "isUnique": true,
4017 + "concurrently": false,
4018 + "method": "btree",
4019 + "with": {}
4020 + },
4021 + "listings_asset_avail_idx": {
4022 + "name": "listings_asset_avail_idx",
4023 + "columns": [
4024 + {
4025 + "expression": "asset_id",
4026 + "isExpression": false,
4027 + "asc": true,
4028 + "nulls": "last"
4029 + },
4030 + {
4031 + "expression": "availability",
4032 + "isExpression": false,
4033 + "asc": true,
4034 + "nulls": "last"
4035 + }
4036 + ],
4037 + "isUnique": false,
4038 + "concurrently": false,
4039 + "method": "btree",
4040 + "with": {}
4041 + },
4042 + "listings_avail_price_idx": {
4043 + "name": "listings_avail_price_idx",
4044 + "columns": [
4045 + {
4046 + "expression": "availability",
4047 + "isExpression": false,
4048 + "asc": true,
4049 + "nulls": "last"
4050 + },
4051 + {
4052 + "expression": "price_usd",
4053 + "isExpression": false,
4054 + "asc": true,
4055 + "nulls": "last"
4056 + }
4057 + ],
4058 + "isUnique": false,
4059 + "concurrently": false,
4060 + "method": "btree",
4061 + "with": {}
4062 + },
4063 + "listings_ends_idx": {
4064 + "name": "listings_ends_idx",
4065 + "columns": [
4066 + {
4067 + "expression": "ends_at",
4068 + "isExpression": false,
4069 + "asc": true,
4070 + "nulls": "last"
4071 + }
4072 + ],
4073 + "isUnique": false,
4074 + "concurrently": false,
4075 + "method": "btree",
4076 + "with": {}
4077 + },
4078 + "listings_last_seen_idx": {
4079 + "name": "listings_last_seen_idx",
4080 + "columns": [
4081 + {
4082 + "expression": "last_seen_at",
4083 + "isExpression": false,
4084 + "asc": true,
4085 + "nulls": "last"
4086 + }
4087 + ],
4088 + "isUnique": false,
4089 + "concurrently": false,
4090 + "method": "btree",
4091 + "with": {}
4092 + }
4093 + },
4094 + "foreignKeys": {},
4095 + "compositePrimaryKeys": {},
4096 + "uniqueConstraints": {},
4097 + "policies": {},
4098 + "checkConstraints": {},
4099 + "isRLSEnabled": false
4100 + },
4101 + "public.news": {
4102 + "name": "news",
4103 + "schema": "",
4104 + "columns": {
4105 + "id": {
4106 + "name": "id",
4107 + "type": "text",
4108 + "primaryKey": true,
4109 + "notNull": true
4110 + },
4111 + "source_id": {
4112 + "name": "source_id",
4113 + "type": "text",
4114 + "primaryKey": false,
4115 + "notNull": true
4116 + },
4117 + "url": {
4118 + "name": "url",
4119 + "type": "text",
4120 + "primaryKey": false,
4121 + "notNull": true
4122 + },
4123 + "title": {
4124 + "name": "title",
4125 + "type": "text",
4126 + "primaryKey": false,
4127 + "notNull": true
4128 + },
4129 + "summary": {
4130 + "name": "summary",
4131 + "type": "text",
4132 + "primaryKey": false,
4133 + "notNull": false
4134 + },
4135 + "ai_summary": {
4136 + "name": "ai_summary",
4137 + "type": "text",
4138 + "primaryKey": false,
4139 + "notNull": false
4140 + },
4141 + "published_at": {
4142 + "name": "published_at",
4143 + "type": "timestamp with time zone",
4144 + "primaryKey": false,
4145 + "notNull": false
4146 + },
4147 + "category_slugs": {
4148 + "name": "category_slugs",
4149 + "type": "text[]",
4150 + "primaryKey": false,
4151 + "notNull": true,
4152 + "default": "'{}'::text[]"
4153 + },
4154 + "news_type": {
4155 + "name": "news_type",
4156 + "type": "text",
4157 + "primaryKey": false,
4158 + "notNull": false
4159 + },
4160 + "image_url": {
4161 + "name": "image_url",
4162 + "type": "text",
4163 + "primaryKey": false,
4164 + "notNull": false
4165 + },
4166 + "fetched_at": {
4167 + "name": "fetched_at",
4168 + "type": "timestamp with time zone",
4169 + "primaryKey": false,
4170 + "notNull": true
4171 + }
4172 + },
4173 + "indexes": {
4174 + "news_url_uq": {
4175 + "name": "news_url_uq",
4176 + "columns": [
4177 + {
4178 + "expression": "url",
4179 + "isExpression": false,
4180 + "asc": true,
4181 + "nulls": "last"
4182 + }
4183 + ],
4184 + "isUnique": true,
4185 + "concurrently": false,
4186 + "method": "btree",
4187 + "with": {}
4188 + },
4189 + "news_published_idx": {
4190 + "name": "news_published_idx",
4191 + "columns": [
4192 + {
4193 + "expression": "published_at",
4194 + "isExpression": false,
4195 + "asc": true,
4196 + "nulls": "last"
4197 + }
4198 + ],
4199 + "isUnique": false,
4200 + "concurrently": false,
4201 + "method": "btree",
4202 + "with": {}
4203 + }
4204 + },
4205 + "foreignKeys": {},
4206 + "compositePrimaryKeys": {},
4207 + "uniqueConstraints": {},
4208 + "policies": {},
4209 + "checkConstraints": {},
4210 + "isRLSEnabled": false
4211 + },
4212 + "public.price_observations": {
4213 + "name": "price_observations",
4214 + "schema": "",
4215 + "columns": {
4216 + "id": {
4217 + "name": "id",
4218 + "type": "text",
4219 + "primaryKey": true,
4220 + "notNull": true
4221 + },
4222 + "asset_id": {
4223 + "name": "asset_id",
4224 + "type": "text",
4225 + "primaryKey": false,
4226 + "notNull": true
4227 + },
4228 + "variant_id": {
4229 + "name": "variant_id",
4230 + "type": "text",
4231 + "primaryKey": false,
4232 + "notNull": false
4233 + },
4234 + "source_id": {
4235 + "name": "source_id",
4236 + "type": "text",
4237 + "primaryKey": false,
4238 + "notNull": true
4239 + },
4240 + "connector_id": {
4241 + "name": "connector_id",
4242 + "type": "text",
4243 + "primaryKey": false,
4244 + "notNull": true
4245 + },
4246 + "raw_record_id": {
4247 + "name": "raw_record_id",
4248 + "type": "text",
4249 + "primaryKey": false,
4250 + "notNull": false
4251 + },
4252 + "source_url": {
4253 + "name": "source_url",
4254 + "type": "text",
4255 + "primaryKey": false,
4256 + "notNull": true
4257 + },
4258 + "price_kind": {
4259 + "name": "price_kind",
4260 + "type": "text",
4261 + "primaryKey": false,
4262 + "notNull": true
4263 + },
4264 + "price": {
4265 + "name": "price",
4266 + "type": "numeric(18, 4)",
4267 + "primaryKey": false,
4268 + "notNull": true
4269 + },
4270 + "currency": {
4271 + "name": "currency",
4272 + "type": "text",
4273 + "primaryKey": false,
4274 + "notNull": true
4275 + },
4276 + "price_usd": {
4277 + "name": "price_usd",
4278 + "type": "numeric(18, 4)",
4279 + "primaryKey": false,
4280 + "notNull": true
4281 + },
4282 + "observation_date": {
4283 + "name": "observation_date",
4284 + "type": "date",
4285 + "primaryKey": false,
4286 + "notNull": true
4287 + },
4288 + "sample_size": {
4289 + "name": "sample_size",
4290 + "type": "integer",
4291 + "primaryKey": false,
4292 + "notNull": false
4293 + },
4294 + "dedupe_key": {
4295 + "name": "dedupe_key",
4296 + "type": "text",
4297 + "primaryKey": false,
4298 + "notNull": true
4299 + },
4300 + "created_at": {
4301 + "name": "created_at",
4302 + "type": "timestamp with time zone",
4303 + "primaryKey": false,
4304 + "notNull": true,
4305 + "default": "now()"
4306 + }
4307 + },
4308 + "indexes": {
4309 + "price_observations_dedupe_uq": {
4310 + "name": "price_observations_dedupe_uq",
4311 + "columns": [
4312 + {
4313 + "expression": "dedupe_key",
4314 + "isExpression": false,
4315 + "asc": true,
4316 + "nulls": "last"
4317 + }
4318 + ],
4319 + "isUnique": true,
4320 + "concurrently": false,
4321 + "method": "btree",
4322 + "with": {}
4323 + },
4324 + "price_observations_asset_date_idx": {
4325 + "name": "price_observations_asset_date_idx",
4326 + "columns": [
4327 + {
4328 + "expression": "asset_id",
4329 + "isExpression": false,
4330 + "asc": true,
4331 + "nulls": "last"
4332 + },
4333 + {
4334 + "expression": "observation_date",
4335 + "isExpression": false,
4336 + "asc": true,
4337 + "nulls": "last"
4338 + }
4339 + ],
4340 + "isUnique": false,
4341 + "concurrently": false,
4342 + "method": "btree",
4343 + "with": {}
4344 + }
4345 + },
4346 + "foreignKeys": {},
4347 + "compositePrimaryKeys": {},
4348 + "uniqueConstraints": {},
4349 + "policies": {},
4350 + "checkConstraints": {},
4351 + "isRLSEnabled": false
4352 + },
4353 + "public.sales": {
4354 + "name": "sales",
4355 + "schema": "",
4356 + "columns": {
4357 + "id": {
4358 + "name": "id",
4359 + "type": "text",
4360 + "primaryKey": true,
4361 + "notNull": true
4362 + },
4363 + "asset_id": {
4364 + "name": "asset_id",
4365 + "type": "text",
4366 + "primaryKey": false,
4367 + "notNull": true
4368 + },
4369 + "variant_id": {
4370 + "name": "variant_id",
4371 + "type": "text",
4372 + "primaryKey": false,
4373 + "notNull": false
4374 + },
4375 + "source_id": {
4376 + "name": "source_id",
4377 + "type": "text",
4378 + "primaryKey": false,
4379 + "notNull": true
4380 + },
4381 + "connector_id": {
4382 + "name": "connector_id",
4383 + "type": "text",
4384 + "primaryKey": false,
4385 + "notNull": true
4386 + },
4387 + "raw_record_id": {
4388 + "name": "raw_record_id",
4389 + "type": "text",
4390 + "primaryKey": false,
4391 + "notNull": false
4392 + },
4393 + "normalized_record_id": {
4394 + "name": "normalized_record_id",
4395 + "type": "text",
4396 + "primaryKey": false,
4397 + "notNull": false
4398 + },
4399 + "source_url": {
4400 + "name": "source_url",
4401 + "type": "text",
4402 + "primaryKey": false,
4403 + "notNull": true
4404 + },
4405 + "external_id": {
4406 + "name": "external_id",
4407 + "type": "text",
4408 + "primaryKey": false,
4409 + "notNull": false
4410 + },
4411 + "sale_type": {
4412 + "name": "sale_type",
4413 + "type": "text",
4414 + "primaryKey": false,
4415 + "notNull": true,
4416 + "default": "'unknown'"
4417 + },
4418 + "sale_date": {
4419 + "name": "sale_date",
4420 + "type": "timestamp with time zone",
4421 + "primaryKey": false,
4422 + "notNull": true
4423 + },
4424 + "price": {
4425 + "name": "price",
4426 + "type": "numeric(18, 4)",
4427 + "primaryKey": false,
4428 + "notNull": true
4429 + },
4430 + "currency": {
4431 + "name": "currency",
4432 + "type": "text",
4433 + "primaryKey": false,
4434 + "notNull": true
4435 + },
4436 + "price_usd": {
4437 + "name": "price_usd",
4438 + "type": "numeric(18, 4)",
4439 + "primaryKey": false,
4440 + "notNull": true
4441 + },
4442 + "fx_rate": {
4443 + "name": "fx_rate",
4444 + "type": "real",
4445 + "primaryKey": false,
4446 + "notNull": false
4447 + },
4448 + "fx_date": {
4449 + "name": "fx_date",
4450 + "type": "date",
4451 + "primaryKey": false,
4452 + "notNull": false
4453 + },
4454 + "buyer_premium_included": {
4455 + "name": "buyer_premium_included",
4456 + "type": "boolean",
4457 + "primaryKey": false,
4458 + "notNull": false
4459 + },
4460 + "quantity": {
4461 + "name": "quantity",
4462 + "type": "integer",
4463 + "primaryKey": false,
4464 + "notNull": true,
4465 + "default": 1
4466 + },
4467 + "is_bundle": {
4468 + "name": "is_bundle",
4469 + "type": "boolean",
4470 + "primaryKey": false,
4471 + "notNull": true,
4472 + "default": false
4473 + },
4474 + "condition": {
4475 + "name": "condition",
4476 + "type": "text",
4477 + "primaryKey": false,
4478 + "notNull": false
4479 + },
4480 + "grader": {
4481 + "name": "grader",
4482 + "type": "text",
4483 + "primaryKey": false,
4484 + "notNull": false
4485 + },
4486 + "grade": {
4487 + "name": "grade",
4488 + "type": "text",
4489 + "primaryKey": false,
4490 + "notNull": false
4491 + },
4492 + "certification_number": {
4493 + "name": "certification_number",
4494 + "type": "text",
4495 + "primaryKey": false,
4496 + "notNull": false
4497 + },
4498 + "location": {
4499 + "name": "location",
4500 + "type": "text",
4501 + "primaryKey": false,
4502 + "notNull": false
4503 + },
4504 + "auction_house": {
4505 + "name": "auction_house",
4506 + "type": "text",
4507 + "primaryKey": false,
4508 + "notNull": false
4509 + },
4510 + "lot_number": {
4511 + "name": "lot_number",
4512 + "type": "text",
4513 + "primaryKey": false,
4514 + "notNull": false
4515 + },
4516 + "image_urls": {
4517 + "name": "image_urls",
4518 + "type": "jsonb",
4519 + "primaryKey": false,
4520 + "notNull": true,
4521 + "default": "'[]'::jsonb"
4522 + },
4523 + "raw_title": {
4524 + "name": "raw_title",
4525 + "type": "text",
4526 + "primaryKey": false,
4527 + "notNull": true
4528 + },
4529 + "confidence": {
4530 + "name": "confidence",
4531 + "type": "numeric(8, 6)",
4532 + "primaryKey": false,
4533 + "notNull": true,
4534 + "default": 0.8
4535 + },
4536 + "data_quality": {
4537 + "name": "data_quality",
4538 + "type": "real",
4539 + "primaryKey": false,
4540 + "notNull": true,
4541 + "default": 0
4542 + },
4543 + "status": {
4544 + "name": "status",
4545 + "type": "text",
4546 + "primaryKey": false,
4547 + "notNull": true,
4548 + "default": "'valid'"
4549 + },
4550 + "flags": {
4551 + "name": "flags",
4552 + "type": "text[]",
4553 + "primaryKey": false,
4554 + "notNull": true,
4555 + "default": "'{}'::text[]"
4556 + },
4557 + "dedupe_key": {
4558 + "name": "dedupe_key",
4559 + "type": "text",
4560 + "primaryKey": false,
4561 + "notNull": true
4562 + },
4563 + "created_at": {
4564 + "name": "created_at",
4565 + "type": "timestamp with time zone",
4566 + "primaryKey": false,
4567 + "notNull": true,
4568 + "default": "now()"
4569 + }
4570 + },
4571 + "indexes": {
4572 + "sales_dedupe_uq": {
4573 + "name": "sales_dedupe_uq",
4574 + "columns": [
4575 + {
4576 + "expression": "dedupe_key",
4577 + "isExpression": false,
4578 + "asc": true,
4579 + "nulls": "last"
4580 + }
4581 + ],
4582 + "isUnique": true,
4583 + "concurrently": false,
4584 + "method": "btree",
4585 + "with": {}
4586 + },
4587 + "sales_asset_date_idx": {
4588 + "name": "sales_asset_date_idx",
4589 + "columns": [
4590 + {
4591 + "expression": "asset_id",
4592 + "isExpression": false,
4593 + "asc": true,
4594 + "nulls": "last"
4595 + },
4596 + {
4597 + "expression": "sale_date",
4598 + "isExpression": false,
4599 + "asc": true,
4600 + "nulls": "last"
4601 + }
4602 + ],
4603 + "isUnique": false,
4604 + "concurrently": false,
4605 + "method": "btree",
4606 + "with": {}
4607 + },
4608 + "sales_variant_date_idx": {
4609 + "name": "sales_variant_date_idx",
4610 + "columns": [
4611 + {
4612 + "expression": "variant_id",
4613 + "isExpression": false,
4614 + "asc": true,
4615 + "nulls": "last"
4616 + },
4617 + {
4618 + "expression": "sale_date",
4619 + "isExpression": false,
4620 + "asc": true,
4621 + "nulls": "last"
4622 + }
4623 + ],
4624 + "isUnique": false,
4625 + "concurrently": false,
4626 + "method": "btree",
4627 + "with": {}
4628 + },
4629 + "sales_source_idx": {
4630 + "name": "sales_source_idx",
4631 + "columns": [
4632 + {
4633 + "expression": "source_id",
4634 + "isExpression": false,
4635 + "asc": true,
4636 + "nulls": "last"
4637 + },
4638 + {
4639 + "expression": "sale_date",
4640 + "isExpression": false,
4641 + "asc": true,
4642 + "nulls": "last"
4643 + }
4644 + ],
4645 + "isUnique": false,
4646 + "concurrently": false,
4647 + "method": "btree",
4648 + "with": {}
4649 + },
4650 + "sales_date_idx": {
4651 + "name": "sales_date_idx",
4652 + "columns": [
4653 + {
4654 + "expression": "sale_date",
4655 + "isExpression": false,
4656 + "asc": true,
4657 + "nulls": "last"
4658 + }
4659 + ],
4660 + "isUnique": false,
4661 + "concurrently": false,
4662 + "method": "btree",
4663 + "with": {}
4664 + },
4665 + "sales_price_idx": {
4666 + "name": "sales_price_idx",
4667 + "columns": [
4668 + {
4669 + "expression": "price_usd",
4670 + "isExpression": false,
4671 + "asc": true,
4672 + "nulls": "last"
4673 + }
4674 + ],
4675 + "isUnique": false,
4676 + "concurrently": false,
4677 + "method": "btree",
4678 + "with": {}
4679 + }
4680 + },
4681 + "foreignKeys": {},
4682 + "compositePrimaryKeys": {},
4683 + "uniqueConstraints": {},
4684 + "policies": {},
4685 + "checkConstraints": {},
4686 + "isRLSEnabled": false
4687 + },
4688 + "public.benchmarks": {
4689 + "name": "benchmarks",
4690 + "schema": "",
4691 + "columns": {
4692 + "ticker": {
4693 + "name": "ticker",
4694 + "type": "text",
4695 + "primaryKey": false,
4696 + "notNull": true
4697 + },
4698 + "date": {
4699 + "name": "date",
4700 + "type": "date",
4701 + "primaryKey": false,
4702 + "notNull": true
4703 + },
4704 + "value": {
4705 + "name": "value",
4706 + "type": "real",
4707 + "primaryKey": false,
4708 + "notNull": true
4709 + },
4710 + "source": {
4711 + "name": "source",
4712 + "type": "text",
4713 + "primaryKey": false,
4714 + "notNull": true
4715 + }
4716 + },
4717 + "indexes": {},
4718 + "foreignKeys": {},
4719 + "compositePrimaryKeys": {
4720 + "benchmarks_ticker_date_pk": {
4721 + "name": "benchmarks_ticker_date_pk",
4722 + "columns": [
4723 + "ticker",
4724 + "date"
4725 + ]
4726 + }
4727 + },
4728 + "uniqueConstraints": {},
4729 + "policies": {},
4730 + "checkConstraints": {},
4731 + "isRLSEnabled": false
4732 + },
4733 + "public.category_snapshots": {
4734 + "name": "category_snapshots",
4735 + "schema": "",
4736 + "columns": {
4737 + "category_slug": {
4738 + "name": "category_slug",
4739 + "type": "text",
4740 + "primaryKey": false,
4741 + "notNull": true
4742 + },
4743 + "date": {
4744 + "name": "date",
4745 + "type": "date",
4746 + "primaryKey": false,
4747 + "notNull": true
4748 + },
4749 + "index_value": {
4750 + "name": "index_value",
4751 + "type": "real",
4752 + "primaryKey": false,
4753 + "notNull": false
4754 + },
4755 + "tracked_assets": {
4756 + "name": "tracked_assets",
4757 + "type": "integer",
4758 + "primaryKey": false,
4759 + "notNull": true,
4760 + "default": 0
4761 + },
4762 + "assets_with_valuation": {
4763 + "name": "assets_with_valuation",
4764 + "type": "integer",
4765 + "primaryKey": false,
4766 + "notNull": true,
4767 + "default": 0
4768 + },
4769 + "sales": {
4770 + "name": "sales",
4771 + "type": "integer",
4772 + "primaryKey": false,
4773 + "notNull": true,
4774 + "default": 0
4775 + },
4776 + "volume_usd": {
4777 + "name": "volume_usd",
4778 + "type": "numeric(18, 4)",
4779 + "primaryKey": false,
4780 + "notNull": false
4781 + },
4782 + "median_sale_usd": {
4783 + "name": "median_sale_usd",
4784 + "type": "numeric(18, 4)",
4785 + "primaryKey": false,
4786 + "notNull": false
4787 + },
4788 + "active_listings": {
4789 + "name": "active_listings",
4790 + "type": "integer",
4791 + "primaryKey": false,
4792 + "notNull": true,
4793 + "default": 0
4794 + },
4795 + "market_cap_est_usd": {
4796 + "name": "market_cap_est_usd",
4797 + "type": "numeric(18, 4)",
4798 + "primaryKey": false,
4799 + "notNull": false
4800 + },
4801 + "liquidity_score": {
4802 + "name": "liquidity_score",
4803 + "type": "real",
4804 + "primaryKey": false,
4805 + "notNull": false
4806 + },
4807 + "change_1d": {
4808 + "name": "change_1d",
4809 + "type": "numeric(8, 6)",
4810 + "primaryKey": false,
4811 + "notNull": false
4812 + },
4813 + "change_7d": {
4814 + "name": "change_7d",
4815 + "type": "numeric(8, 6)",
4816 + "primaryKey": false,
4817 + "notNull": false
4818 + },
4819 + "change_30d": {
4820 + "name": "change_30d",
4821 + "type": "numeric(8, 6)",
4822 + "primaryKey": false,
4823 + "notNull": false
4824 + },
4825 + "change_1y": {
4826 + "name": "change_1y",
4827 + "type": "numeric(8, 6)",
4828 + "primaryKey": false,
4829 + "notNull": false
4830 + }
4831 + },
4832 + "indexes": {},
4833 + "foreignKeys": {},
4834 + "compositePrimaryKeys": {
4835 + "category_snapshots_category_slug_date_pk": {
4836 + "name": "category_snapshots_category_slug_date_pk",
4837 + "columns": [
4838 + "category_slug",
4839 + "date"
4840 + ]
4841 + }
4842 + },
4843 + "uniqueConstraints": {},
4844 + "policies": {},
4845 + "checkConstraints": {},
4846 + "isRLSEnabled": false
4847 + },
4848 + "public.correlations": {
4849 + "name": "correlations",
4850 + "schema": "",
4851 + "columns": {
4852 + "a": {
4853 + "name": "a",
4854 + "type": "text",
4855 + "primaryKey": false,
4856 + "notNull": true
4857 + },
4858 + "b": {
4859 + "name": "b",
4860 + "type": "text",
4861 + "primaryKey": false,
4862 + "notNull": true
4863 + },
4864 + "window_days": {
4865 + "name": "window_days",
4866 + "type": "integer",
4867 + "primaryKey": false,
4868 + "notNull": true
4869 + },
4870 + "coefficient": {
4871 + "name": "coefficient",
4872 + "type": "real",
4873 + "primaryKey": false,
4874 + "notNull": true
4875 + },
4876 + "observations": {
4877 + "name": "observations",
4878 + "type": "integer",
4879 + "primaryKey": false,
4880 + "notNull": true
4881 + },
4882 + "computed_at": {
4883 + "name": "computed_at",
4884 + "type": "timestamp with time zone",
4885 + "primaryKey": false,
4886 + "notNull": true
4887 + }
4888 + },
4889 + "indexes": {},
4890 + "foreignKeys": {},
4891 + "compositePrimaryKeys": {
4892 + "correlations_a_b_window_days_pk": {
4893 + "name": "correlations_a_b_window_days_pk",
4894 + "columns": [
4895 + "a",
4896 + "b",
4897 + "window_days"
4898 + ]
4899 + }
4900 + },
4901 + "uniqueConstraints": {},
4902 + "policies": {},
4903 + "checkConstraints": {},
4904 + "isRLSEnabled": false
4905 + },
4906 + "public.index_constituents": {
4907 + "name": "index_constituents",
4908 + "schema": "",
4909 + "columns": {
4910 + "index_id": {
4911 + "name": "index_id",
4912 + "type": "text",
4913 + "primaryKey": false,
4914 + "notNull": true
4915 + },
4916 + "asset_id": {
4917 + "name": "asset_id",
4918 + "type": "text",
4919 + "primaryKey": false,
4920 + "notNull": true
4921 + },
4922 + "variant_id": {
4923 + "name": "variant_id",
4924 + "type": "text",
4925 + "primaryKey": false,
4926 + "notNull": true,
4927 + "default": "''"
4928 + },
4929 + "weight": {
4930 + "name": "weight",
4931 + "type": "real",
4932 + "primaryKey": false,
4933 + "notNull": true,
4934 + "default": 1
4935 + },
4936 + "added_at": {
4937 + "name": "added_at",
4938 + "type": "date",
4939 + "primaryKey": false,
4940 + "notNull": true
4941 + },
4942 + "removed_at": {
4943 + "name": "removed_at",
4944 + "type": "date",
4945 + "primaryKey": false,
4946 + "notNull": false
4947 + },
4948 + "reason": {
4949 + "name": "reason",
4950 + "type": "text",
4951 + "primaryKey": false,
4952 + "notNull": false
4953 + }
4954 + },
4955 + "indexes": {
4956 + "index_constituents_asset_idx": {
4957 + "name": "index_constituents_asset_idx",
4958 + "columns": [
4959 + {
4960 + "expression": "asset_id",
4961 + "isExpression": false,
4962 + "asc": true,
4963 + "nulls": "last"
4964 + }
4965 + ],
4966 + "isUnique": false,
4967 + "concurrently": false,
4968 + "method": "btree",
4969 + "with": {}
4970 + }
4971 + },
4972 + "foreignKeys": {},
4973 + "compositePrimaryKeys": {
4974 + "index_constituents_index_id_asset_id_variant_id_added_at_pk": {
4975 + "name": "index_constituents_index_id_asset_id_variant_id_added_at_pk",
4976 + "columns": [
4977 + "index_id",
4978 + "asset_id",
4979 + "variant_id",
4980 + "added_at"
4981 + ]
4982 + }
4983 + },
4984 + "uniqueConstraints": {},
4985 + "policies": {},
4986 + "checkConstraints": {},
4987 + "isRLSEnabled": false
4988 + },
4989 + "public.index_values": {
4990 + "name": "index_values",
4991 + "schema": "",
4992 + "columns": {
4993 + "index_id": {
4994 + "name": "index_id",
4995 + "type": "text",
4996 + "primaryKey": false,
4997 + "notNull": true
4998 + },
4999 + "date": {
5000 + "name": "date",
5001 + "type": "date",
5002 + "primaryKey": false,
5003 + "notNull": true
5004 + },
5005 + "value": {
5006 + "name": "value",
5007 + "type": "real",
5008 + "primaryKey": false,
5009 + "notNull": true
5010 + },
5011 + "constituents_count": {
5012 + "name": "constituents_count",
5013 + "type": "integer",
5014 + "primaryKey": false,
5015 + "notNull": true,
5016 + "default": 0
5017 + },
5018 + "transactions": {
5019 + "name": "transactions",
5020 + "type": "integer",
5021 + "primaryKey": false,
5022 + "notNull": true,
5023 + "default": 0
5024 + },
5025 + "volume_usd": {
5026 + "name": "volume_usd",
5027 + "type": "numeric(18, 4)",
5028 + "primaryKey": false,
5029 + "notNull": false
5030 + },
5031 + "median_sale_usd": {
5032 + "name": "median_sale_usd",
5033 + "type": "numeric(18, 4)",
5034 + "primaryKey": false,
5035 + "notNull": false
5036 + },
5037 + "avg_sale_usd": {
5038 + "name": "avg_sale_usd",
5039 + "type": "numeric(18, 4)",
5040 + "primaryKey": false,
5041 + "notNull": false
5042 + },
5043 + "market_cap_est_usd": {
5044 + "name": "market_cap_est_usd",
5045 + "type": "numeric(18, 4)",
5046 + "primaryKey": false,
5047 + "notNull": false
5048 + },
5049 + "market_cap_confidence": {
5050 + "name": "market_cap_confidence",
5051 + "type": "text",
5052 + "primaryKey": false,
5053 + "notNull": false
5054 + },
5055 + "liquidity_score": {
5056 + "name": "liquidity_score",
5057 + "type": "real",
5058 + "primaryKey": false,
5059 + "notNull": false
5060 + },
5061 + "momentum": {
5062 + "name": "momentum",
5063 + "type": "real",
5064 + "primaryKey": false,
5065 + "notNull": false
5066 + },
5067 + "breadth": {
5068 + "name": "breadth",
5069 + "type": "integer",
5070 + "primaryKey": false,
5071 + "notNull": false
5072 + },
5073 + "tracked_assets": {
5074 + "name": "tracked_assets",
5075 + "type": "integer",
5076 + "primaryKey": false,
5077 + "notNull": false
5078 + },
5079 + "coverage": {
5080 + "name": "coverage",
5081 + "type": "real",
5082 + "primaryKey": false,
5083 + "notNull": false
5084 + }
5085 + },
5086 + "indexes": {},
5087 + "foreignKeys": {},
5088 + "compositePrimaryKeys": {
5089 + "index_values_index_id_date_pk": {
5090 + "name": "index_values_index_id_date_pk",
5091 + "columns": [
5092 + "index_id",
5093 + "date"
5094 + ]
5095 + }
5096 + },
5097 + "uniqueConstraints": {},
5098 + "policies": {},
5099 + "checkConstraints": {},
5100 + "isRLSEnabled": false
5101 + },
5102 + "public.indices": {
5103 + "name": "indices",
5104 + "schema": "",
5105 + "columns": {
5106 + "id": {
5107 + "name": "id",
5108 + "type": "text",
5109 + "primaryKey": true,
5110 + "notNull": true
5111 + },
5112 + "ticker": {
5113 + "name": "ticker",
5114 + "type": "text",
5115 + "primaryKey": false,
5116 + "notNull": true
5117 + },
5118 + "name": {
5119 + "name": "name",
5120 + "type": "text",
5121 + "primaryKey": false,
5122 + "notNull": true
5123 + },
5124 + "description": {
5125 + "name": "description",
5126 + "type": "text",
5127 + "primaryKey": false,
5128 + "notNull": false
5129 + },
5130 + "category_slugs": {
5131 + "name": "category_slugs",
5132 + "type": "text[]",
5133 + "primaryKey": false,
5134 + "notNull": true,
5135 + "default": "'{}'::text[]"
5136 + },
5137 + "family_slugs": {
5138 + "name": "family_slugs",
5139 + "type": "text[]",
5140 + "primaryKey": false,
5141 + "notNull": true,
5142 + "default": "'{}'::text[]"
5143 + },
5144 + "parent_ticker": {
5145 + "name": "parent_ticker",
5146 + "type": "text",
5147 + "primaryKey": false,
5148 + "notNull": false
5149 + },
5150 + "methodology": {
5151 + "name": "methodology",
5152 + "type": "text",
5153 + "primaryKey": false,
5154 + "notNull": true,
5155 + "default": "'chain_linked_equal_weight_v1'"
5156 + },
5157 + "weighting": {
5158 + "name": "weighting",
5159 + "type": "text",
5160 + "primaryKey": false,
5161 + "notNull": true,
5162 + "default": "'equal'"
5163 + },
5164 + "base_date": {
5165 + "name": "base_date",
5166 + "type": "date",
5167 + "primaryKey": false,
5168 + "notNull": true
5169 + },
5170 + "base_value": {
5171 + "name": "base_value",
5172 + "type": "real",
5173 + "primaryKey": false,
5174 + "notNull": true,
5175 + "default": 1000
5176 + },
5177 + "min_constituents": {
5178 + "name": "min_constituents",
5179 + "type": "integer",
5180 + "primaryKey": false,
5181 + "notNull": true,
5182 + "default": 10
5183 + },
5184 + "active": {
5185 + "name": "active",
5186 + "type": "boolean",
5187 + "primaryKey": false,
5188 + "notNull": true,
5189 + "default": true
5190 + },
5191 + "is_flagship": {
5192 + "name": "is_flagship",
5193 + "type": "boolean",
5194 + "primaryKey": false,
5195 + "notNull": true,
5196 + "default": false
5197 + },
5198 + "color": {
5199 + "name": "color",
5200 + "type": "text",
5201 + "primaryKey": false,
5202 + "notNull": false
5203 + },
5204 + "created_at": {
5205 + "name": "created_at",
5206 + "type": "timestamp with time zone",
5207 + "primaryKey": false,
5208 + "notNull": true,
5209 + "default": "now()"
5210 + }
5211 + },
5212 + "indexes": {},
5213 + "foreignKeys": {},
5214 + "compositePrimaryKeys": {},
5215 + "uniqueConstraints": {
5216 + "indices_ticker_unique": {
5217 + "name": "indices_ticker_unique",
5218 + "nullsNotDistinct": false,
5219 + "columns": [
5220 + "ticker"
5221 + ]
5222 + }
5223 + },
5224 + "policies": {},
5225 + "checkConstraints": {},
5226 + "isRLSEnabled": false
5227 + },
5228 + "public.price_snapshots": {
5229 + "name": "price_snapshots",
5230 + "schema": "",
5231 + "columns": {
5232 + "asset_id": {
5233 + "name": "asset_id",
5234 + "type": "text",
5235 + "primaryKey": false,
5236 + "notNull": true
5237 + },
5238 + "variant_id": {
5239 + "name": "variant_id",
5240 + "type": "text",
5241 + "primaryKey": false,
5242 + "notNull": true,
5243 + "default": "''"
5244 + },
5245 + "date": {
5246 + "name": "date",
5247 + "type": "date",
5248 + "primaryKey": false,
5249 + "notNull": true
5250 + },
5251 + "riv_usd": {
5252 + "name": "riv_usd",
5253 + "type": "numeric(18, 4)",
5254 + "primaryKey": false,
5255 + "notNull": false
5256 + },
5257 + "latest_sale_usd": {
5258 + "name": "latest_sale_usd",
5259 + "type": "numeric(18, 4)",
5260 + "primaryKey": false,
5261 + "notNull": false
5262 + },
5263 + "median_usd": {
5264 + "name": "median_usd",
5265 + "type": "numeric(18, 4)",
5266 + "primaryKey": false,
5267 + "notNull": false
5268 + },
5269 + "sales_count": {
5270 + "name": "sales_count",
5271 + "type": "integer",
5272 + "primaryKey": false,
5273 + "notNull": true,
5274 + "default": 0
5275 + },
5276 + "volume_usd": {
5277 + "name": "volume_usd",
5278 + "type": "numeric(18, 4)",
5279 + "primaryKey": false,
5280 + "notNull": false
5281 + },
5282 + "listings_count": {
5283 + "name": "listings_count",
5284 + "type": "integer",
5285 + "primaryKey": false,
5286 + "notNull": true,
5287 + "default": 0
5288 + },
5289 + "min_ask_usd": {
5290 + "name": "min_ask_usd",
5291 + "type": "numeric(18, 4)",
5292 + "primaryKey": false,
5293 + "notNull": false
5294 + },
5295 + "observation_usd": {
5296 + "name": "observation_usd",
5297 + "type": "numeric(18, 4)",
5298 + "primaryKey": false,
5299 + "notNull": false
5300 + }
5301 + },
5302 + "indexes": {
5303 + "price_snapshots_date_idx": {
5304 + "name": "price_snapshots_date_idx",
5305 + "columns": [
5306 + {
5307 + "expression": "date",
5308 + "isExpression": false,
5309 + "asc": true,
5310 + "nulls": "last"
5311 + }
5312 + ],
5313 + "isUnique": false,
5314 + "concurrently": false,
5315 + "method": "btree",
5316 + "with": {}
5317 + }
5318 + },
5319 + "foreignKeys": {},
5320 + "compositePrimaryKeys": {
5321 + "price_snapshots_asset_id_variant_id_date_pk": {
5322 + "name": "price_snapshots_asset_id_variant_id_date_pk",
5323 + "columns": [
5324 + "asset_id",
5325 + "variant_id",
5326 + "date"
5327 + ]
5328 + }
5329 + },
5330 + "uniqueConstraints": {},
5331 + "policies": {},
5332 + "checkConstraints": {},
5333 + "isRLSEnabled": false
5334 + },
5335 + "public.radar_findings": {
5336 + "name": "radar_findings",
5337 + "schema": "",
5338 + "columns": {
5339 + "id": {
5340 + "name": "id",
5341 + "type": "text",
5342 + "primaryKey": true,
5343 + "notNull": true
5344 + },
5345 + "asset_id": {
5346 + "name": "asset_id",
5347 + "type": "text",
5348 + "primaryKey": false,
5349 + "notNull": true
5350 + },
5351 + "kind": {
5352 + "name": "kind",
5353 + "type": "text",
5354 + "primaryKey": false,
5355 + "notNull": true
5356 + },
5357 + "score": {
5358 + "name": "score",
5359 + "type": "real",
5360 + "primaryKey": false,
5361 + "notNull": true
5362 + },
5363 + "evidence": {
5364 + "name": "evidence",
5365 + "type": "jsonb",
5366 + "primaryKey": false,
5367 + "notNull": true,
5368 + "default": "'{}'::jsonb"
5369 + },
5370 + "entity_type": {
5371 + "name": "entity_type",
5372 + "type": "text",
5373 + "primaryKey": false,
5374 + "notNull": false
5375 + },
5376 + "entity_id": {
5377 + "name": "entity_id",
5378 + "type": "text",
5379 + "primaryKey": false,
5380 + "notNull": false
5381 + },
5382 + "detected_at": {
5383 + "name": "detected_at",
5384 + "type": "timestamp with time zone",
5385 + "primaryKey": false,
5386 + "notNull": true
5387 + },
5388 + "expires_at": {
5389 + "name": "expires_at",
5390 + "type": "timestamp with time zone",
5391 + "primaryKey": false,
5392 + "notNull": false
5393 + }
5394 + },
5395 + "indexes": {
5396 + "radar_kind_idx": {
5397 + "name": "radar_kind_idx",
5398 + "columns": [
5399 + {
5400 + "expression": "kind",
5401 + "isExpression": false,
5402 + "asc": true,
5403 + "nulls": "last"
5404 + },
5405 + {
5406 + "expression": "detected_at",
5407 + "isExpression": false,
5408 + "asc": true,
5409 + "nulls": "last"
5410 + }
5411 + ],
5412 + "isUnique": false,
5413 + "concurrently": false,
5414 + "method": "btree",
5415 + "with": {}
5416 + },
5417 + "radar_entity_uq": {
5418 + "name": "radar_entity_uq",
5419 + "columns": [
5420 + {
5421 + "expression": "kind",
5422 + "isExpression": false,
5423 + "asc": true,
5424 + "nulls": "last"
5425 + },
5426 + {
5427 + "expression": "entity_type",
5428 + "isExpression": false,
5429 + "asc": true,
5430 + "nulls": "last"
5431 + },
5432 + {
5433 + "expression": "entity_id",
5434 + "isExpression": false,
5435 + "asc": true,
5436 + "nulls": "last"
5437 + }
5438 + ],
5439 + "isUnique": true,
5440 + "concurrently": false,
5441 + "method": "btree",
5442 + "with": {}
5443 + }
5444 + },
5445 + "foreignKeys": {},
5446 + "compositePrimaryKeys": {},
5447 + "uniqueConstraints": {},
5448 + "policies": {},
5449 + "checkConstraints": {},
5450 + "isRLSEnabled": false
5451 + },
5452 + "public.valuations": {
5453 + "name": "valuations",
5454 + "schema": "",
5455 + "columns": {
5456 + "id": {
5457 + "name": "id",
5458 + "type": "text",
5459 + "primaryKey": true,
5460 + "notNull": true
5461 + },
5462 + "asset_id": {
5463 + "name": "asset_id",
5464 + "type": "text",
5465 + "primaryKey": false,
5466 + "notNull": true
5467 + },
5468 + "variant_id": {
5469 + "name": "variant_id",
5470 + "type": "text",
5471 + "primaryKey": false,
5472 + "notNull": false
5473 + },
5474 + "computed_at": {
5475 + "name": "computed_at",
5476 + "type": "timestamp with time zone",
5477 + "primaryKey": false,
5478 + "notNull": true
5479 + },
5480 + "riv_usd": {
5481 + "name": "riv_usd",
5482 + "type": "numeric(18, 4)",
5483 + "primaryKey": false,
5484 + "notNull": false
5485 + },
5486 + "low_usd": {
5487 + "name": "low_usd",
5488 + "type": "numeric(18, 4)",
5489 + "primaryKey": false,
5490 + "notNull": false
5491 + },
5492 + "high_usd": {
5493 + "name": "high_usd",
5494 + "type": "numeric(18, 4)",
5495 + "primaryKey": false,
5496 + "notNull": false
5497 + },
5498 + "confidence": {
5499 + "name": "confidence",
5500 + "type": "numeric(8, 6)",
5501 + "primaryKey": false,
5502 + "notNull": true,
5503 + "default": 0
5504 + },
5505 + "confidence_label": {
5506 + "name": "confidence_label",
5507 + "type": "text",
5508 + "primaryKey": false,
5509 + "notNull": true,
5510 + "default": "'insufficient'"
5511 + },
5512 + "sample_size": {
5513 + "name": "sample_size",
5514 + "type": "integer",
5515 + "primaryKey": false,
5516 + "notNull": true,
5517 + "default": 0
5518 + },
5519 + "window_days": {
5520 + "name": "window_days",
5521 + "type": "integer",
5522 + "primaryKey": false,
5523 + "notNull": true,
5524 + "default": 365
5525 + },
5526 + "methods": {
5527 + "name": "methods",
5528 + "type": "jsonb",
5529 + "primaryKey": false,
5530 + "notNull": true,
5531 + "default": "'{}'::jsonb"
5532 + },
5533 + "sales_used": {
5534 + "name": "sales_used",
5535 + "type": "text[]",
5536 + "primaryKey": false,
5537 + "notNull": true,
5538 + "default": "'{}'::text[]"
5539 + },
5540 + "observations_used": {
5541 + "name": "observations_used",
5542 + "type": "integer",
5543 + "primaryKey": false,
5544 + "notNull": true,
5545 + "default": 0
5546 + },
5547 + "method": {
5548 + "name": "method",
5549 + "type": "text",
5550 + "primaryKey": false,
5551 + "notNull": true,
5552 + "default": "'ensemble_v1'"
5553 + },
5554 + "notes": {
5555 + "name": "notes",
5556 + "type": "text[]",
5557 + "primaryKey": false,
5558 + "notNull": true,
5559 + "default": "'{}'::text[]"
5560 + }
5561 + },
5562 + "indexes": {
5563 + "valuations_asset_idx": {
5564 + "name": "valuations_asset_idx",
5565 + "columns": [
5566 + {
5567 + "expression": "asset_id",
5568 + "isExpression": false,
5569 + "asc": true,
5570 + "nulls": "last"
5571 + },
5572 + {
5573 + "expression": "computed_at",
5574 + "isExpression": false,
5575 + "asc": true,
5576 + "nulls": "last"
5577 + }
5578 + ],
5579 + "isUnique": false,
5580 + "concurrently": false,
5581 + "method": "btree",
5582 + "with": {}
5583 + },
5584 + "valuations_variant_idx": {
5585 + "name": "valuations_variant_idx",
5586 + "columns": [
5587 + {
5588 + "expression": "variant_id",
5589 + "isExpression": false,
5590 + "asc": true,
5591 + "nulls": "last"
5592 + },
5593 + {
5594 + "expression": "computed_at",
5595 + "isExpression": false,
5596 + "asc": true,
5597 + "nulls": "last"
5598 + }
5599 + ],
5600 + "isUnique": false,
5601 + "concurrently": false,
5602 + "method": "btree",
5603 + "with": {}
5604 + }
5605 + },
5606 + "foreignKeys": {},
5607 + "compositePrimaryKeys": {},
5608 + "uniqueConstraints": {},
5609 + "policies": {},
5610 + "checkConstraints": {},
5611 + "isRLSEnabled": false
5612 + },
5613 + "public.alert_events": {
5614 + "name": "alert_events",
5615 + "schema": "",
5616 + "columns": {
5617 + "id": {
5618 + "name": "id",
5619 + "type": "text",
5620 + "primaryKey": true,
5621 + "notNull": true
5622 + },
5623 + "alert_id": {
5624 + "name": "alert_id",
5625 + "type": "text",
5626 + "primaryKey": false,
5627 + "notNull": true
5628 + },
5629 + "user_id": {
5630 + "name": "user_id",
5631 + "type": "text",
5632 + "primaryKey": false,
5633 + "notNull": true
5634 + },
5635 + "message": {
5636 + "name": "message",
5637 + "type": "text",
5638 + "primaryKey": false,
5639 + "notNull": true
5640 + },
5641 + "payload": {
5642 + "name": "payload",
5643 + "type": "jsonb",
5644 + "primaryKey": false,
5645 + "notNull": true,
5646 + "default": "'{}'::jsonb"
5647 + },
5648 + "read_at": {
5649 + "name": "read_at",
5650 + "type": "timestamp with time zone",
5651 + "primaryKey": false,
5652 + "notNull": false
5653 + },
5654 + "created_at": {
5655 + "name": "created_at",
5656 + "type": "timestamp with time zone",
5657 + "primaryKey": false,
5658 + "notNull": true,
5659 + "default": "now()"
5660 + }
5661 + },
5662 + "indexes": {
5663 + "alert_events_user_idx": {
5664 + "name": "alert_events_user_idx",
5665 + "columns": [
5666 + {
5667 + "expression": "user_id",
5668 + "isExpression": false,
5669 + "asc": true,
5670 + "nulls": "last"
5671 + },
5672 + {
5673 + "expression": "created_at",
5674 + "isExpression": false,
5675 + "asc": true,
5676 + "nulls": "last"
5677 + }
5678 + ],
5679 + "isUnique": false,
5680 + "concurrently": false,
5681 + "method": "btree",
5682 + "with": {}
5683 + }
5684 + },
5685 + "foreignKeys": {},
5686 + "compositePrimaryKeys": {},
5687 + "uniqueConstraints": {},
5688 + "policies": {},
5689 + "checkConstraints": {},
5690 + "isRLSEnabled": false
5691 + },
5692 + "public.alerts": {
5693 + "name": "alerts",
5694 + "schema": "",
5695 + "columns": {
5696 + "id": {
5697 + "name": "id",
5698 + "type": "text",
5699 + "primaryKey": true,
5700 + "notNull": true
5701 + },
5702 + "user_id": {
5703 + "name": "user_id",
5704 + "type": "text",
5705 + "primaryKey": false,
5706 + "notNull": true
5707 + },
5708 + "alert_type": {
5709 + "name": "alert_type",
5710 + "type": "text",
5711 + "primaryKey": false,
5712 + "notNull": true
5713 + },
5714 + "target_type": {
5715 + "name": "target_type",
5716 + "type": "text",
5717 + "primaryKey": false,
5718 + "notNull": true
5719 + },
5720 + "target_id": {
5721 + "name": "target_id",
5722 + "type": "text",
5723 + "primaryKey": false,
5724 + "notNull": true
5725 + },
5726 + "threshold": {
5727 + "name": "threshold",
5728 + "type": "numeric(18, 4)",
5729 + "primaryKey": false,
5730 + "notNull": false
5731 + },
5732 + "currency": {
5733 + "name": "currency",
5734 + "type": "text",
5735 + "primaryKey": false,
5736 + "notNull": false
5737 + },
5738 + "params": {
5739 + "name": "params",
5740 + "type": "jsonb",
5741 + "primaryKey": false,
5742 + "notNull": true,
5743 + "default": "'{}'::jsonb"
5744 + },
5745 + "channel": {
5746 + "name": "channel",
5747 + "type": "text",
5748 + "primaryKey": false,
5749 + "notNull": true,
5750 + "default": "'inapp'"
5751 + },
5752 + "active": {
5753 + "name": "active",
5754 + "type": "boolean",
5755 + "primaryKey": false,
5756 + "notNull": true,
5757 + "default": true
5758 + },
5759 + "name": {
5760 + "name": "name",
5761 + "type": "text",
5762 + "primaryKey": false,
5763 + "notNull": false
5764 + },
5765 + "cooldown_minutes": {
5766 + "name": "cooldown_minutes",
5767 + "type": "integer",
5768 + "primaryKey": false,
5769 + "notNull": true,
5770 + "default": 1440
5771 + },
5772 + "last_triggered_at": {
5773 + "name": "last_triggered_at",
5774 + "type": "timestamp with time zone",
5775 + "primaryKey": false,
5776 + "notNull": false
5777 + },
5778 + "trigger_count": {
5779 + "name": "trigger_count",
5780 + "type": "integer",
5781 + "primaryKey": false,
5782 + "notNull": true,
5783 + "default": 0
5784 + },
5785 + "created_at": {
5786 + "name": "created_at",
5787 + "type": "timestamp with time zone",
5788 + "primaryKey": false,
5789 + "notNull": true,
5790 + "default": "now()"
5791 + }
5792 + },
5793 + "indexes": {
5794 + "alerts_user_idx": {
5795 + "name": "alerts_user_idx",
5796 + "columns": [
5797 + {
5798 + "expression": "user_id",
5799 + "isExpression": false,
5800 + "asc": true,
5801 + "nulls": "last"
5802 + }
5803 + ],
5804 + "isUnique": false,
5805 + "concurrently": false,
5806 + "method": "btree",
5807 + "with": {}
5808 + },
5809 + "alerts_target_idx": {
5810 + "name": "alerts_target_idx",
5811 + "columns": [
5812 + {
5813 + "expression": "target_type",
5814 + "isExpression": false,
5815 + "asc": true,
5816 + "nulls": "last"
5817 + },
5818 + {
5819 + "expression": "target_id",
5820 + "isExpression": false,
5821 + "asc": true,
5822 + "nulls": "last"
5823 + },
5824 + {
5825 + "expression": "active",
5826 + "isExpression": false,
5827 + "asc": true,
5828 + "nulls": "last"
5829 + }
5830 + ],
5831 + "isUnique": false,
5832 + "concurrently": false,
5833 + "method": "btree",
5834 + "with": {}
5835 + }
5836 + },
5837 + "foreignKeys": {},
5838 + "compositePrimaryKeys": {},
5839 + "uniqueConstraints": {},
5840 + "policies": {},
5841 + "checkConstraints": {},
5842 + "isRLSEnabled": false
5843 + },
5844 + "public.api_keys": {
5845 + "name": "api_keys",
5846 + "schema": "",
5847 + "columns": {
5848 + "id": {
5849 + "name": "id",
5850 + "type": "text",
5851 + "primaryKey": true,
5852 + "notNull": true
5853 + },
5854 + "user_id": {
5855 + "name": "user_id",
5856 + "type": "text",
5857 + "primaryKey": false,
5858 + "notNull": false
5859 + },
5860 + "name": {
5861 + "name": "name",
5862 + "type": "text",
5863 + "primaryKey": false,
5864 + "notNull": true
5865 + },
5866 + "prefix": {
5867 + "name": "prefix",
5868 + "type": "text",
5869 + "primaryKey": false,
5870 + "notNull": true
5871 + },
5872 + "key_hash": {
5873 + "name": "key_hash",
5874 + "type": "text",
5875 + "primaryKey": false,
5876 + "notNull": true
5877 + },
5878 + "tier": {
5879 + "name": "tier",
5880 + "type": "text",
5881 + "primaryKey": false,
5882 + "notNull": true,
5883 + "default": "'free'"
5884 + },
5885 + "rate_limit_per_minute": {
5886 + "name": "rate_limit_per_minute",
5887 + "type": "integer",
5888 + "primaryKey": false,
5889 + "notNull": true,
5890 + "default": 60
5891 + },
5892 + "daily_quota": {
5893 + "name": "daily_quota",
5894 + "type": "integer",
5895 + "primaryKey": false,
5896 + "notNull": true,
5897 + "default": 1000
5898 + },
5899 + "created_at": {
5900 + "name": "created_at",
5901 + "type": "timestamp with time zone",
5902 + "primaryKey": false,
5903 + "notNull": true,
5904 + "default": "now()"
5905 + },
5906 + "last_used_at": {
5907 + "name": "last_used_at",
5908 + "type": "timestamp with time zone",
5909 + "primaryKey": false,
5910 + "notNull": false
5911 + },
5912 + "revoked_at": {
5913 + "name": "revoked_at",
5914 + "type": "timestamp with time zone",
5915 + "primaryKey": false,
5916 + "notNull": false
5917 + }
5918 + },
5919 + "indexes": {
5920 + "api_keys_user_idx": {
5921 + "name": "api_keys_user_idx",
5922 + "columns": [
5923 + {
5924 + "expression": "user_id",
5925 + "isExpression": false,
5926 + "asc": true,
5927 + "nulls": "last"
5928 + }
5929 + ],
5930 + "isUnique": false,
5931 + "concurrently": false,
5932 + "method": "btree",
5933 + "with": {}
5934 + }
5935 + },
5936 + "foreignKeys": {},
5937 + "compositePrimaryKeys": {},
5938 + "uniqueConstraints": {
5939 + "api_keys_key_hash_unique": {
5940 + "name": "api_keys_key_hash_unique",
5941 + "nullsNotDistinct": false,
5942 + "columns": [
5943 + "key_hash"
5944 + ]
5945 + }
5946 + },
5947 + "policies": {},
5948 + "checkConstraints": {},
5949 + "isRLSEnabled": false
5950 + },
5951 + "public.api_usage": {
5952 + "name": "api_usage",
5953 + "schema": "",
5954 + "columns": {
5955 + "key_id": {
5956 + "name": "key_id",
5957 + "type": "text",
5958 + "primaryKey": false,
5959 + "notNull": true
5960 + },
5961 + "date": {
5962 + "name": "date",
5963 + "type": "date",
5964 + "primaryKey": false,
5965 + "notNull": true
5966 + },
5967 + "endpoint": {
5968 + "name": "endpoint",
5969 + "type": "text",
5970 + "primaryKey": false,
5971 + "notNull": true
5972 + },
5973 + "count": {
5974 + "name": "count",
5975 + "type": "integer",
5976 + "primaryKey": false,
5977 + "notNull": true,
5978 + "default": 0
5979 + },
5980 + "latency_ms_avg": {
5981 + "name": "latency_ms_avg",
5982 + "type": "real",
5983 + "primaryKey": false,
5984 + "notNull": false
5985 + }
5986 + },
5987 + "indexes": {},
5988 + "foreignKeys": {},
5989 + "compositePrimaryKeys": {
5990 + "api_usage_key_id_date_endpoint_pk": {
5991 + "name": "api_usage_key_id_date_endpoint_pk",
5992 + "columns": [
5993 + "key_id",
5994 + "date",
5995 + "endpoint"
5996 + ]
5997 + }
5998 + },
5999 + "uniqueConstraints": {},
6000 + "policies": {},
6001 + "checkConstraints": {},
6002 + "isRLSEnabled": false
6003 + },
6004 + "public.asset_views": {
6005 + "name": "asset_views",
6006 + "schema": "",
6007 + "columns": {
6008 + "asset_id": {
6009 + "name": "asset_id",
6010 + "type": "text",
6011 + "primaryKey": false,
6012 + "notNull": true
6013 + },
6014 + "date": {
6015 + "name": "date",
6016 + "type": "date",
6017 + "primaryKey": false,
6018 + "notNull": true
6019 + },
6020 + "views": {
6021 + "name": "views",
6022 + "type": "integer",
6023 + "primaryKey": false,
6024 + "notNull": true,
6025 + "default": 0
6026 + }
6027 + },
6028 + "indexes": {},
6029 + "foreignKeys": {},
6030 + "compositePrimaryKeys": {
6031 + "asset_views_asset_id_date_pk": {
6032 + "name": "asset_views_asset_id_date_pk",
6033 + "columns": [
6034 + "asset_id",
6035 + "date"
6036 + ]
6037 + }
6038 + },
6039 + "uniqueConstraints": {},
6040 + "policies": {},
6041 + "checkConstraints": {},
6042 + "isRLSEnabled": false
6043 + },
6044 + "public.collection_items": {
6045 + "name": "collection_items",
6046 + "schema": "",
6047 + "columns": {
6048 + "id": {
6049 + "name": "id",
6050 + "type": "text",
6051 + "primaryKey": true,
6052 + "notNull": true
6053 + },
6054 + "collection_id": {
6055 + "name": "collection_id",
6056 + "type": "text",
6057 + "primaryKey": false,
6058 + "notNull": true
6059 + },
6060 + "asset_id": {
6061 + "name": "asset_id",
6062 + "type": "text",
6063 + "primaryKey": false,
6064 + "notNull": true
6065 + },
6066 + "variant_id": {
6067 + "name": "variant_id",
6068 + "type": "text",
6069 + "primaryKey": false,
6070 + "notNull": false
6071 + },
6072 + "quantity": {
6073 + "name": "quantity",
6074 + "type": "integer",
6075 + "primaryKey": false,
6076 + "notNull": true,
6077 + "default": 1
6078 + },
6079 + "acquired_at": {
6080 + "name": "acquired_at",
6081 + "type": "date",
6082 + "primaryKey": false,
6083 + "notNull": false
6084 + },
6085 + "purchase_price": {
6086 + "name": "purchase_price",
6087 + "type": "numeric(18, 4)",
6088 + "primaryKey": false,
6089 + "notNull": false
6090 + },
6091 + "purchase_currency": {
6092 + "name": "purchase_currency",
6093 + "type": "text",
6094 + "primaryKey": false,
6095 + "notNull": false
6096 + },
6097 + "purchase_price_usd": {
6098 + "name": "purchase_price_usd",
6099 + "type": "numeric(18, 4)",
6100 + "primaryKey": false,
6101 + "notNull": false
6102 + },
6103 + "source": {
6104 + "name": "source",
6105 + "type": "text",
6106 + "primaryKey": false,
6107 + "notNull": false
6108 + },
6109 + "grader": {
6110 + "name": "grader",
6111 + "type": "text",
6112 + "primaryKey": false,
6113 + "notNull": false
6114 + },
6115 + "grade": {
6116 + "name": "grade",
6117 + "type": "text",
6118 + "primaryKey": false,
6119 + "notNull": false
6120 + },
6121 + "certification_number": {
6122 + "name": "certification_number",
6123 + "type": "text",
6124 + "primaryKey": false,
6125 + "notNull": false
6126 + },
6127 + "serial": {
6128 + "name": "serial",
6129 + "type": "text",
6130 + "primaryKey": false,
6131 + "notNull": false
6132 + },
6133 + "photos": {
6134 + "name": "photos",
6135 + "type": "jsonb",
6136 + "primaryKey": false,
6137 + "notNull": true,
6138 + "default": "'[]'::jsonb"
6139 + },
6140 + "notes": {
6141 + "name": "notes",
6142 + "type": "text",
6143 + "primaryKey": false,
6144 + "notNull": false
6145 + },
6146 + "tags": {
6147 + "name": "tags",
6148 + "type": "jsonb",
6149 + "primaryKey": false,
6150 + "notNull": true,
6151 + "default": "'[]'::jsonb"
6152 + },
6153 + "condition": {
6154 + "name": "condition",
6155 + "type": "text",
6156 + "primaryKey": false,
6157 + "notNull": false
6158 + },
6159 + "manual_value_usd": {
6160 + "name": "manual_value_usd",
6161 + "type": "numeric(18, 4)",
6162 + "primaryKey": false,
6163 + "notNull": false
6164 + },
6165 + "sold_at": {
6166 + "name": "sold_at",
6167 + "type": "date",
6168 + "primaryKey": false,
6169 + "notNull": false
6170 + },
6171 + "sold_price_usd": {
6172 + "name": "sold_price_usd",
6173 + "type": "numeric(18, 4)",
6174 + "primaryKey": false,
6175 + "notNull": false
6176 + },
6177 + "created_at": {
6178 + "name": "created_at",
6179 + "type": "timestamp with time zone",
6180 + "primaryKey": false,
6181 + "notNull": true,
6182 + "default": "now()"
6183 + },
6184 + "updated_at": {
6185 + "name": "updated_at",
6186 + "type": "timestamp with time zone",
6187 + "primaryKey": false,
6188 + "notNull": true,
6189 + "default": "now()"
6190 + }
6191 + },
6192 + "indexes": {
6193 + "collection_items_collection_idx": {
6194 + "name": "collection_items_collection_idx",
6195 + "columns": [
6196 + {
6197 + "expression": "collection_id",
6198 + "isExpression": false,
6199 + "asc": true,
6200 + "nulls": "last"
6201 + }
6202 + ],
6203 + "isUnique": false,
6204 + "concurrently": false,
6205 + "method": "btree",
6206 + "with": {}
6207 + },
6208 + "collection_items_asset_idx": {
6209 + "name": "collection_items_asset_idx",
6210 + "columns": [
6211 + {
6212 + "expression": "asset_id",
6213 + "isExpression": false,
6214 + "asc": true,
6215 + "nulls": "last"
6216 + }
6217 + ],
6218 + "isUnique": false,
6219 + "concurrently": false,
6220 + "method": "btree",
6221 + "with": {}
6222 + }
6223 + },
6224 + "foreignKeys": {},
6225 + "compositePrimaryKeys": {},
6226 + "uniqueConstraints": {},
6227 + "policies": {},
6228 + "checkConstraints": {},
6229 + "isRLSEnabled": false
6230 + },
6231 + "public.collection_snapshots": {
6232 + "name": "collection_snapshots",
6233 + "schema": "",
6234 + "columns": {
6235 + "collection_id": {
6236 + "name": "collection_id",
6237 + "type": "text",
6238 + "primaryKey": false,
6239 + "notNull": true
6240 + },
6241 + "date": {
6242 + "name": "date",
6243 + "type": "date",
6244 + "primaryKey": false,
6245 + "notNull": true
6246 + },
6247 + "value_usd": {
6248 + "name": "value_usd",
6249 + "type": "numeric(18, 4)",
6250 + "primaryKey": false,
6251 + "notNull": true
6252 + },
6253 + "cost_basis_usd": {
6254 + "name": "cost_basis_usd",
6255 + "type": "numeric(18, 4)",
6256 + "primaryKey": false,
6257 + "notNull": true
6258 + },
6259 + "items": {
6260 + "name": "items",
6261 + "type": "integer",
6262 + "primaryKey": false,
6263 + "notNull": true
6264 + }
6265 + },
6266 + "indexes": {},
6267 + "foreignKeys": {},
6268 + "compositePrimaryKeys": {
6269 + "collection_snapshots_collection_id_date_pk": {
6270 + "name": "collection_snapshots_collection_id_date_pk",
6271 + "columns": [
6272 + "collection_id",
6273 + "date"
6274 + ]
6275 + }
6276 + },
6277 + "uniqueConstraints": {},
6278 + "policies": {},
6279 + "checkConstraints": {},
6280 + "isRLSEnabled": false
6281 + },
6282 + "public.collections": {
6283 + "name": "collections",
6284 + "schema": "",
6285 + "columns": {
6286 + "id": {
6287 + "name": "id",
6288 + "type": "text",
6289 + "primaryKey": true,
6290 + "notNull": true
6291 + },
6292 + "user_id": {
6293 + "name": "user_id",
6294 + "type": "text",
6295 + "primaryKey": false,
6296 + "notNull": true
6297 + },
6298 + "name": {
6299 + "name": "name",
6300 + "type": "text",
6301 + "primaryKey": false,
6302 + "notNull": true
6303 + },
6304 + "description": {
6305 + "name": "description",
6306 + "type": "text",
6307 + "primaryKey": false,
6308 + "notNull": false
6309 + },
6310 + "is_public": {
6311 + "name": "is_public",
6312 + "type": "boolean",
6313 + "primaryKey": false,
6314 + "notNull": true,
6315 + "default": false
6316 + },
6317 + "public_slug": {
6318 + "name": "public_slug",
6319 + "type": "text",
6320 + "primaryKey": false,
6321 + "notNull": false
6322 + },
6323 + "kind": {
6324 + "name": "kind",
6325 + "type": "text",
6326 + "primaryKey": false,
6327 + "notNull": true,
6328 + "default": "'collection'"
6329 + },
6330 + "budget_usd": {
6331 + "name": "budget_usd",
6332 + "type": "numeric(18, 4)",
6333 + "primaryKey": false,
6334 + "notNull": false
6335 + },
6336 + "color": {
6337 + "name": "color",
6338 + "type": "text",
6339 + "primaryKey": false,
6340 + "notNull": false
6341 + },
6342 + "created_at": {
6343 + "name": "created_at",
6344 + "type": "timestamp with time zone",
6345 + "primaryKey": false,
6346 + "notNull": true,
6347 + "default": "now()"
6348 + },
6349 + "updated_at": {
6350 + "name": "updated_at",
6351 + "type": "timestamp with time zone",
6352 + "primaryKey": false,
6353 + "notNull": true,
6354 + "default": "now()"
6355 + }
6356 + },
6357 + "indexes": {
6358 + "collections_user_idx": {
6359 + "name": "collections_user_idx",
6360 + "columns": [
6361 + {
6362 + "expression": "user_id",
6363 + "isExpression": false,
6364 + "asc": true,
6365 + "nulls": "last"
6366 + }
6367 + ],
6368 + "isUnique": false,
6369 + "concurrently": false,
6370 + "method": "btree",
6371 + "with": {}
6372 + },
6373 + "collections_public_slug_uq": {
6374 + "name": "collections_public_slug_uq",
6375 + "columns": [
6376 + {
6377 + "expression": "public_slug",
6378 + "isExpression": false,
6379 + "asc": true,
6380 + "nulls": "last"
6381 + }
6382 + ],
6383 + "isUnique": true,
6384 + "concurrently": false,
6385 + "method": "btree",
6386 + "with": {}
6387 + }
6388 + },
6389 + "foreignKeys": {},
6390 + "compositePrimaryKeys": {},
6391 + "uniqueConstraints": {},
6392 + "policies": {},
6393 + "checkConstraints": {},
6394 + "isRLSEnabled": false
6395 + },
6396 + "public.search_log": {
6397 + "name": "search_log",
6398 + "schema": "",
6399 + "columns": {
6400 + "id": {
6401 + "name": "id",
6402 + "type": "text",
6403 + "primaryKey": true,
6404 + "notNull": true
6405 + },
6406 + "query": {
6407 + "name": "query",
6408 + "type": "text",
6409 + "primaryKey": false,
6410 + "notNull": true
6411 + },
6412 + "normalized": {
6413 + "name": "normalized",
6414 + "type": "text",
6415 + "primaryKey": false,
6416 + "notNull": true
6417 + },
6418 + "results": {
6419 + "name": "results",
6420 + "type": "integer",
6421 + "primaryKey": false,
6422 + "notNull": true
6423 + },
6424 + "user_id": {
6425 + "name": "user_id",
6426 + "type": "text",
6427 + "primaryKey": false,
6428 + "notNull": false
6429 + },
6430 + "created_at": {
6431 + "name": "created_at",
6432 + "type": "timestamp with time zone",
6433 + "primaryKey": false,
6434 + "notNull": true,
6435 + "default": "now()"
6436 + }
6437 + },
6438 + "indexes": {
6439 + "search_log_created_idx": {
6440 + "name": "search_log_created_idx",
6441 + "columns": [
6442 + {
6443 + "expression": "created_at",
6444 + "isExpression": false,
6445 + "asc": true,
6446 + "nulls": "last"
6447 + }
6448 + ],
6449 + "isUnique": false,
6450 + "concurrently": false,
6451 + "method": "btree",
6452 + "with": {}
6453 + },
6454 + "search_log_normalized_idx": {
6455 + "name": "search_log_normalized_idx",
6456 + "columns": [
6457 + {
6458 + "expression": "normalized",
6459 + "isExpression": false,
6460 + "asc": true,
6461 + "nulls": "last"
6462 + }
6463 + ],
6464 + "isUnique": false,
6465 + "concurrently": false,
6466 + "method": "btree",
6467 + "with": {}
6468 + }
6469 + },
6470 + "foreignKeys": {},
6471 + "compositePrimaryKeys": {},
6472 + "uniqueConstraints": {},
6473 + "policies": {},
6474 + "checkConstraints": {},
6475 + "isRLSEnabled": false
6476 + },
6477 + "public.sessions": {
6478 + "name": "sessions",
6479 + "schema": "",
6480 + "columns": {
6481 + "id": {
6482 + "name": "id",
6483 + "type": "text",
6484 + "primaryKey": true,
6485 + "notNull": true
6486 + },
6487 + "user_id": {
6488 + "name": "user_id",
6489 + "type": "text",
6490 + "primaryKey": false,
6491 + "notNull": true
6492 + },
6493 + "expires_at": {
6494 + "name": "expires_at",
6495 + "type": "timestamp with time zone",
6496 + "primaryKey": false,
6497 + "notNull": true
6498 + },
6499 + "user_agent": {
6500 + "name": "user_agent",
6501 + "type": "text",
6502 + "primaryKey": false,
6503 + "notNull": false
6504 + },
6505 + "ip": {
6506 + "name": "ip",
6507 + "type": "text",
6508 + "primaryKey": false,
6509 + "notNull": false
6510 + },
6511 + "last_seen_at": {
6512 + "name": "last_seen_at",
6513 + "type": "timestamp with time zone",
6514 + "primaryKey": false,
6515 + "notNull": false
6516 + },
6517 + "revoked_at": {
6518 + "name": "revoked_at",
6519 + "type": "timestamp with time zone",
6520 + "primaryKey": false,
6521 + "notNull": false
6522 + },
6523 + "created_at": {
6524 + "name": "created_at",
6525 + "type": "timestamp with time zone",
6526 + "primaryKey": false,
6527 + "notNull": true,
6528 + "default": "now()"
6529 + }
6530 + },
6531 + "indexes": {
6532 + "sessions_user_idx": {
6533 + "name": "sessions_user_idx",
6534 + "columns": [
6535 + {
6536 + "expression": "user_id",
6537 + "isExpression": false,
6538 + "asc": true,
6539 + "nulls": "last"
6540 + }
6541 + ],
6542 + "isUnique": false,
6543 + "concurrently": false,
6544 + "method": "btree",
6545 + "with": {}
6546 + }
6547 + },
6548 + "foreignKeys": {},
6549 + "compositePrimaryKeys": {},
6550 + "uniqueConstraints": {},
6551 + "policies": {},
6552 + "checkConstraints": {},
6553 + "isRLSEnabled": false
6554 + },
6555 + "public.users": {
6556 + "name": "users",
6557 + "schema": "",
6558 + "columns": {
6559 + "id": {
6560 + "name": "id",
6561 + "type": "text",
6562 + "primaryKey": true,
6563 + "notNull": true
6564 + },
6565 + "email": {
6566 + "name": "email",
6567 + "type": "text",
6568 + "primaryKey": false,
6569 + "notNull": true
6570 + },
6571 + "email_verified_at": {
6572 + "name": "email_verified_at",
6573 + "type": "timestamp with time zone",
6574 + "primaryKey": false,
6575 + "notNull": false
6576 + },
6577 + "password_hash": {
6578 + "name": "password_hash",
6579 + "type": "text",
6580 + "primaryKey": false,
6581 + "notNull": false
6582 + },
6583 + "name": {
6584 + "name": "name",
6585 + "type": "text",
6586 + "primaryKey": false,
6587 + "notNull": false
6588 + },
6589 + "role": {
6590 + "name": "role",
6591 + "type": "text",
6592 + "primaryKey": false,
6593 + "notNull": true,
6594 + "default": "'user'"
6595 + },
6596 + "display_currency": {
6597 + "name": "display_currency",
6598 + "type": "text",
6599 + "primaryKey": false,
6600 + "notNull": true,
6601 + "default": "'USD'"
6602 + },
6603 + "providers": {
6604 + "name": "providers",
6605 + "type": "jsonb",
6606 + "primaryKey": false,
6607 + "notNull": true,
6608 + "default": "'[]'::jsonb"
6609 + },
6610 + "preferences": {
6611 + "name": "preferences",
6612 + "type": "jsonb",
6613 + "primaryKey": false,
6614 + "notNull": true,
6615 + "default": "'{}'::jsonb"
6616 + },
6617 + "handle": {
6618 + "name": "handle",
6619 + "type": "text",
6620 + "primaryKey": false,
6621 + "notNull": false
6622 + },
6623 + "avatar_url": {
6624 + "name": "avatar_url",
6625 + "type": "text",
6626 + "primaryKey": false,
6627 + "notNull": false
6628 + },
6629 + "bio": {
6630 + "name": "bio",
6631 + "type": "text",
6632 + "primaryKey": false,
6633 + "notNull": false
6634 + },
6635 + "mfa_enabled": {
6636 + "name": "mfa_enabled",
6637 + "type": "boolean",
6638 + "primaryKey": false,
6639 + "notNull": true,
6640 + "default": false
6641 + },
6642 + "totp_secret_enc": {
6643 + "name": "totp_secret_enc",
6644 + "type": "text",
6645 + "primaryKey": false,
6646 + "notNull": false
6647 + },
6648 + "always_ask_code": {
6649 + "name": "always_ask_code",
6650 + "type": "boolean",
6651 + "primaryKey": false,
6652 + "notNull": true,
6653 + "default": true
6654 + },
6655 + "password_changed_at": {
6656 + "name": "password_changed_at",
6657 + "type": "timestamp with time zone",
6658 + "primaryKey": false,
6659 + "notNull": false
6660 + },
6661 + "pending_email": {
6662 + "name": "pending_email",
6663 + "type": "text",
6664 + "primaryKey": false,
6665 + "notNull": false
6666 + },
6667 + "deleted_at": {
6668 + "name": "deleted_at",
6669 + "type": "timestamp with time zone",
6670 + "primaryKey": false,
6671 + "notNull": false
6672 + },
6673 + "purge_after": {
6674 + "name": "purge_after",
6675 + "type": "timestamp with time zone",
6676 + "primaryKey": false,
6677 + "notNull": false
6678 + },
6679 + "created_at": {
6680 + "name": "created_at",
6681 + "type": "timestamp with time zone",
6682 + "primaryKey": false,
6683 + "notNull": true,
6684 + "default": "now()"
6685 + },
6686 + "last_login_at": {
6687 + "name": "last_login_at",
6688 + "type": "timestamp with time zone",
6689 + "primaryKey": false,
6690 + "notNull": false
6691 + }
6692 + },
6693 + "indexes": {},
6694 + "foreignKeys": {},
6695 + "compositePrimaryKeys": {},
6696 + "uniqueConstraints": {
6697 + "users_email_unique": {
6698 + "name": "users_email_unique",
6699 + "nullsNotDistinct": false,
6700 + "columns": [
6701 + "email"
6702 + ]
6703 + },
6704 + "users_handle_unique": {
6705 + "name": "users_handle_unique",
6706 + "nullsNotDistinct": false,
6707 + "columns": [
6708 + "handle"
6709 + ]
6710 + }
6711 + },
6712 + "policies": {},
6713 + "checkConstraints": {},
6714 + "isRLSEnabled": false
6715 + },
6716 + "public.watchlist_items": {
6717 + "name": "watchlist_items",
6718 + "schema": "",
6719 + "columns": {
6720 + "id": {
6721 + "name": "id",
6722 + "type": "text",
6723 + "primaryKey": true,
6724 + "notNull": true
6725 + },
6726 + "watchlist_id": {
6727 + "name": "watchlist_id",
6728 + "type": "text",
6729 + "primaryKey": false,
6730 + "notNull": true
6731 + },
6732 + "target_type": {
6733 + "name": "target_type",
6734 + "type": "text",
6735 + "primaryKey": false,
6736 + "notNull": true
6737 + },
6738 + "target_id": {
6739 + "name": "target_id",
6740 + "type": "text",
6741 + "primaryKey": false,
6742 + "notNull": true
6743 + },
6744 + "label": {
6745 + "name": "label",
6746 + "type": "text",
6747 + "primaryKey": false,
6748 + "notNull": false
6749 + },
6750 + "note": {
6751 + "name": "note",
6752 + "type": "text",
6753 + "primaryKey": false,
6754 + "notNull": false
6755 + },
6756 + "target_price_usd": {
6757 + "name": "target_price_usd",
6758 + "type": "numeric(18, 4)",
6759 + "primaryKey": false,
6760 + "notNull": false
6761 + },
6762 + "baseline_usd": {
6763 + "name": "baseline_usd",
6764 + "type": "numeric(18, 4)",
6765 + "primaryKey": false,
6766 + "notNull": false
6767 + },
6768 + "created_at": {
6769 + "name": "created_at",
6770 + "type": "timestamp with time zone",
6771 + "primaryKey": false,
6772 + "notNull": true,
6773 + "default": "now()"
6774 + }
6775 + },
6776 + "indexes": {
6777 + "watchlist_items_uq": {
6778 + "name": "watchlist_items_uq",
6779 + "columns": [
6780 + {
6781 + "expression": "watchlist_id",
6782 + "isExpression": false,
6783 + "asc": true,
6784 + "nulls": "last"
6785 + },
6786 + {
6787 + "expression": "target_type",
6788 + "isExpression": false,
6789 + "asc": true,
6790 + "nulls": "last"
6791 + },
6792 + {
6793 + "expression": "target_id",
6794 + "isExpression": false,
6795 + "asc": true,
6796 + "nulls": "last"
6797 + }
6798 + ],
6799 + "isUnique": true,
6800 + "concurrently": false,
6801 + "method": "btree",
6802 + "with": {}
6803 + }
6804 + },
6805 + "foreignKeys": {},
6806 + "compositePrimaryKeys": {},
6807 + "uniqueConstraints": {},
6808 + "policies": {},
6809 + "checkConstraints": {},
6810 + "isRLSEnabled": false
6811 + },
6812 + "public.watchlists": {
6813 + "name": "watchlists",
6814 + "schema": "",
6815 + "columns": {
6816 + "id": {
6817 + "name": "id",
6818 + "type": "text",
6819 + "primaryKey": true,
6820 + "notNull": true
6821 + },
6822 + "user_id": {
6823 + "name": "user_id",
6824 + "type": "text",
6825 + "primaryKey": false,
6826 + "notNull": true
6827 + },
6828 + "name": {
6829 + "name": "name",
6830 + "type": "text",
6831 + "primaryKey": false,
6832 + "notNull": true,
6833 + "default": "'Watchlist'"
6834 + },
6835 + "created_at": {
6836 + "name": "created_at",
6837 + "type": "timestamp with time zone",
6838 + "primaryKey": false,
6839 + "notNull": true,
6840 + "default": "now()"
6841 + }
6842 + },
6843 + "indexes": {
6844 + "watchlists_user_idx": {
6845 + "name": "watchlists_user_idx",
6846 + "columns": [
6847 + {
6848 + "expression": "user_id",
6849 + "isExpression": false,
6850 + "asc": true,
6851 + "nulls": "last"
6852 + }
6853 + ],
6854 + "isUnique": false,
6855 + "concurrently": false,
6856 + "method": "btree",
6857 + "with": {}
6858 + }
6859 + },
6860 + "foreignKeys": {},
6861 + "compositePrimaryKeys": {},
6862 + "uniqueConstraints": {},
6863 + "policies": {},
6864 + "checkConstraints": {},
6865 + "isRLSEnabled": false
6866 + },
6867 + "public.ai_quotas": {
6868 + "name": "ai_quotas",
6869 + "schema": "",
6870 + "columns": {
6871 + "key": {
6872 + "name": "key",
6873 + "type": "text",
6874 + "primaryKey": false,
6875 + "notNull": true
6876 + },
6877 + "feature": {
6878 + "name": "feature",
6879 + "type": "text",
6880 + "primaryKey": false,
6881 + "notNull": true
6882 + },
6883 + "date": {
6884 + "name": "date",
6885 + "type": "text",
6886 + "primaryKey": false,
6887 + "notNull": true
6888 + },
6889 + "count": {
6890 + "name": "count",
6891 + "type": "integer",
6892 + "primaryKey": false,
6893 + "notNull": true,
6894 + "default": 0
6895 + }
6896 + },
6897 + "indexes": {
6898 + "ai_quotas_uq": {
6899 + "name": "ai_quotas_uq",
6900 + "columns": [
6901 + {
6902 + "expression": "key",
6903 + "isExpression": false,
6904 + "asc": true,
6905 + "nulls": "last"
6906 + },
6907 + {
6908 + "expression": "feature",
6909 + "isExpression": false,
6910 + "asc": true,
6911 + "nulls": "last"
6912 + },
6913 + {
6914 + "expression": "date",
6915 + "isExpression": false,
6916 + "asc": true,
6917 + "nulls": "last"
6918 + }
6919 + ],
6920 + "isUnique": false,
6921 + "concurrently": false,
6922 + "method": "btree",
6923 + "with": {}
6924 + }
6925 + },
6926 + "foreignKeys": {},
6927 + "compositePrimaryKeys": {},
6928 + "uniqueConstraints": {},
6929 + "policies": {},
6930 + "checkConstraints": {},
6931 + "isRLSEnabled": false
6932 + },
6933 + "public.research_messages": {
6934 + "name": "research_messages",
6935 + "schema": "",
6936 + "columns": {
6937 + "id": {
6938 + "name": "id",
6939 + "type": "text",
6940 + "primaryKey": true,
6941 + "notNull": true
6942 + },
6943 + "session_id": {
6944 + "name": "session_id",
6945 + "type": "text",
6946 + "primaryKey": false,
6947 + "notNull": true
6948 + },
6949 + "role": {
6950 + "name": "role",
6951 + "type": "text",
6952 + "primaryKey": false,
6953 + "notNull": true
6954 + },
6955 + "content": {
6956 + "name": "content",
6957 + "type": "text",
6958 + "primaryKey": false,
6959 + "notNull": true
6960 + },
6961 + "tool_calls": {
6962 + "name": "tool_calls",
6963 + "type": "jsonb",
6964 + "primaryKey": false,
6965 + "notNull": true,
6966 + "default": "'[]'::jsonb"
6967 + },
6968 + "usage": {
6969 + "name": "usage",
6970 + "type": "jsonb",
6971 + "primaryKey": false,
6972 + "notNull": true,
6973 + "default": "'{}'::jsonb"
6974 + },
6975 + "usd_est": {
6976 + "name": "usd_est",
6977 + "type": "real",
6978 + "primaryKey": false,
6979 + "notNull": true,
6980 + "default": 0
6981 + },
6982 + "model": {
6983 + "name": "model",
6984 + "type": "text",
6985 + "primaryKey": false,
6986 + "notNull": false
6987 + },
6988 + "created_at": {
6989 + "name": "created_at",
6990 + "type": "timestamp with time zone",
6991 + "primaryKey": false,
6992 + "notNull": true,
6993 + "default": "now()"
6994 + }
6995 + },
6996 + "indexes": {
6997 + "research_messages_session_idx": {
6998 + "name": "research_messages_session_idx",
6999 + "columns": [
7000 + {
7001 + "expression": "session_id",
7002 + "isExpression": false,
7003 + "asc": true,
7004 + "nulls": "last"
7005 + },
7006 + {
7007 + "expression": "created_at",
7008 + "isExpression": false,
7009 + "asc": true,
7010 + "nulls": "last"
7011 + }
7012 + ],
7013 + "isUnique": false,
7014 + "concurrently": false,
7015 + "method": "btree",
7016 + "with": {}
7017 + }
7018 + },
7019 + "foreignKeys": {},
7020 + "compositePrimaryKeys": {},
7021 + "uniqueConstraints": {},
7022 + "policies": {},
7023 + "checkConstraints": {},
7024 + "isRLSEnabled": false
7025 + },
7026 + "public.research_sessions": {
7027 + "name": "research_sessions",
7028 + "schema": "",
7029 + "columns": {
7030 + "id": {
7031 + "name": "id",
7032 + "type": "text",
7033 + "primaryKey": true,
7034 + "notNull": true
7035 + },
7036 + "user_id": {
7037 + "name": "user_id",
7038 + "type": "text",
7039 + "primaryKey": false,
7040 + "notNull": false
7041 + },
7042 + "anon_id": {
7043 + "name": "anon_id",
7044 + "type": "text",
7045 + "primaryKey": false,
7046 + "notNull": false
7047 + },
7048 + "title": {
7049 + "name": "title",
7050 + "type": "text",
7051 + "primaryKey": false,
7052 + "notNull": false
7053 + },
7054 + "model": {
7055 + "name": "model",
7056 + "type": "text",
7057 + "primaryKey": false,
7058 + "notNull": false
7059 + },
7060 + "message_count": {
7061 + "name": "message_count",
7062 + "type": "integer",
7063 + "primaryKey": false,
7064 + "notNull": true,
7065 + "default": 0
7066 + },
7067 + "usd_est": {
7068 + "name": "usd_est",
7069 + "type": "real",
7070 + "primaryKey": false,
7071 + "notNull": true,
7072 + "default": 0
7073 + },
7074 + "archived": {
7075 + "name": "archived",
7076 + "type": "boolean",
7077 + "primaryKey": false,
7078 + "notNull": true,
7079 + "default": false
7080 + },
7081 + "created_at": {
7082 + "name": "created_at",
7083 + "type": "timestamp with time zone",
7084 + "primaryKey": false,
7085 + "notNull": true,
7086 + "default": "now()"
7087 + },
7088 + "updated_at": {
7089 + "name": "updated_at",
7090 + "type": "timestamp with time zone",
7091 + "primaryKey": false,
7092 + "notNull": true,
7093 + "default": "now()"
7094 + }
7095 + },
7096 + "indexes": {
7097 + "research_sessions_anon_idx": {
7098 + "name": "research_sessions_anon_idx",
7099 + "columns": [
7100 + {
7101 + "expression": "anon_id",
7102 + "isExpression": false,
7103 + "asc": true,
7104 + "nulls": "last"
7105 + },
7106 + {
7107 + "expression": "updated_at",
7108 + "isExpression": false,
7109 + "asc": true,
7110 + "nulls": "last"
7111 + }
7112 + ],
7113 + "isUnique": false,
7114 + "concurrently": false,
7115 + "method": "btree",
7116 + "with": {}
7117 + },
7118 + "research_sessions_user_idx": {
7119 + "name": "research_sessions_user_idx",
7120 + "columns": [
7121 + {
7122 + "expression": "user_id",
7123 + "isExpression": false,
7124 + "asc": true,
7125 + "nulls": "last"
7126 + },
7127 + {
7128 + "expression": "updated_at",
7129 + "isExpression": false,
7130 + "asc": true,
7131 + "nulls": "last"
7132 + }
7133 + ],
7134 + "isUnique": false,
7135 + "concurrently": false,
7136 + "method": "btree",
7137 + "with": {}
7138 + }
7139 + },
7140 + "foreignKeys": {},
7141 + "compositePrimaryKeys": {},
7142 + "uniqueConstraints": {},
7143 + "policies": {},
7144 + "checkConstraints": {},
7145 + "isRLSEnabled": false
7146 + },
7147 + "public.scanner_sessions": {
7148 + "name": "scanner_sessions",
7149 + "schema": "",
7150 + "columns": {
7151 + "id": {
7152 + "name": "id",
7153 + "type": "text",
7154 + "primaryKey": true,
7155 + "notNull": true
7156 + },
7157 + "user_id": {
7158 + "name": "user_id",
7159 + "type": "text",
7160 + "primaryKey": false,
7161 + "notNull": false
7162 + },
7163 + "anon_id": {
7164 + "name": "anon_id",
7165 + "type": "text",
7166 + "primaryKey": false,
7167 + "notNull": false
7168 + },
7169 + "ip_hash": {
7170 + "name": "ip_hash",
7171 + "type": "text",
7172 + "primaryKey": false,
7173 + "notNull": false
7174 + },
7175 + "mode": {
7176 + "name": "mode",
7177 + "type": "text",
7178 + "primaryKey": false,
7179 + "notNull": true
7180 + },
7181 + "input_url": {
7182 + "name": "input_url",
7183 + "type": "text",
7184 + "primaryKey": false,
7185 + "notNull": false
7186 + },
7187 + "input_text": {
7188 + "name": "input_text",
7189 + "type": "text",
7190 + "primaryKey": false,
7191 + "notNull": false
7192 + },
7193 + "image_count": {
7194 + "name": "image_count",
7195 + "type": "integer",
7196 + "primaryKey": false,
7197 + "notNull": true,
7198 + "default": 0
7199 + },
7200 + "thumbnails": {
7201 + "name": "thumbnails",
7202 + "type": "jsonb",
7203 + "primaryKey": false,
7204 + "notNull": true,
7205 + "default": "'[]'::jsonb"
7206 + },
7207 + "guess": {
7208 + "name": "guess",
7209 + "type": "jsonb",
7210 + "primaryKey": false,
7211 + "notNull": true,
7212 + "default": "'{}'::jsonb"
7213 + },
7214 + "guess_confidence": {
7215 + "name": "guess_confidence",
7216 + "type": "real",
7217 + "primaryKey": false,
7218 + "notNull": false
7219 + },
7220 + "candidates": {
7221 + "name": "candidates",
7222 + "type": "jsonb",
7223 + "primaryKey": false,
7224 + "notNull": true,
7225 + "default": "'[]'::jsonb"
7226 + },
7227 + "chosen_asset_id": {
7228 + "name": "chosen_asset_id",
7229 + "type": "text",
7230 + "primaryKey": false,
7231 + "notNull": false
7232 + },
7233 + "listing": {
7234 + "name": "listing",
7235 + "type": "jsonb",
7236 + "primaryKey": false,
7237 + "notNull": true,
7238 + "default": "'{}'::jsonb"
7239 + },
7240 + "model": {
7241 + "name": "model",
7242 + "type": "text",
7243 + "primaryKey": false,
7244 + "notNull": false
7245 + },
7246 + "usd_est": {
7247 + "name": "usd_est",
7248 + "type": "real",
7249 + "primaryKey": false,
7250 + "notNull": true,
7251 + "default": 0
7252 + },
7253 + "duration_ms": {
7254 + "name": "duration_ms",
7255 + "type": "integer",
7256 + "primaryKey": false,
7257 + "notNull": false
7258 + },
7259 + "error": {
7260 + "name": "error",
7261 + "type": "text",
7262 + "primaryKey": false,
7263 + "notNull": false
7264 + },
7265 + "created_at": {
7266 + "name": "created_at",
7267 + "type": "timestamp with time zone",
7268 + "primaryKey": false,
7269 + "notNull": true,
7270 + "default": "now()"
7271 + },
7272 + "chosen_at": {
7273 + "name": "chosen_at",
7274 + "type": "timestamp with time zone",
7275 + "primaryKey": false,
7276 + "notNull": false
7277 + }
7278 + },
7279 + "indexes": {
7280 + "scanner_sessions_created_idx": {
7281 + "name": "scanner_sessions_created_idx",
7282 + "columns": [
7283 + {
7284 + "expression": "created_at",
7285 + "isExpression": false,
7286 + "asc": true,
7287 + "nulls": "last"
7288 + }
7289 + ],
7290 + "isUnique": false,
7291 + "concurrently": false,
7292 + "method": "btree",
7293 + "with": {}
7294 + },
7295 + "scanner_sessions_ip_idx": {
7296 + "name": "scanner_sessions_ip_idx",
7297 + "columns": [
7298 + {
7299 + "expression": "ip_hash",
7300 + "isExpression": false,
7301 + "asc": true,
7302 + "nulls": "last"
7303 + },
7304 + {
7305 + "expression": "created_at",
7306 + "isExpression": false,
7307 + "asc": true,
7308 + "nulls": "last"
7309 + }
7310 + ],
7311 + "isUnique": false,
7312 + "concurrently": false,
7313 + "method": "btree",
7314 + "with": {}
7315 + },
7316 + "scanner_sessions_user_idx": {
7317 + "name": "scanner_sessions_user_idx",
7318 + "columns": [
7319 + {
7320 + "expression": "user_id",
7321 + "isExpression": false,
7322 + "asc": true,
7323 + "nulls": "last"
7324 + }
7325 + ],
7326 + "isUnique": false,
7327 + "concurrently": false,
7328 + "method": "btree",
7329 + "with": {}
7330 + }
7331 + },
7332 + "foreignKeys": {},
7333 + "compositePrimaryKeys": {},
7334 + "uniqueConstraints": {},
7335 + "policies": {},
7336 + "checkConstraints": {},
7337 + "isRLSEnabled": false
7338 + },
7339 + "public.auth_codes": {
7340 + "name": "auth_codes",
7341 + "schema": "",
7342 + "columns": {
7343 + "id": {
7344 + "name": "id",
7345 + "type": "text",
7346 + "primaryKey": true,
7347 + "notNull": true
7348 + },
7349 + "user_id": {
7350 + "name": "user_id",
7351 + "type": "text",
7352 + "primaryKey": false,
7353 + "notNull": false
7354 + },
7355 + "email": {
7356 + "name": "email",
7357 + "type": "text",
7358 + "primaryKey": false,
7359 + "notNull": true
7360 + },
7361 + "purpose": {
7362 + "name": "purpose",
7363 + "type": "text",
7364 + "primaryKey": false,
7365 + "notNull": true
7366 + },
7367 + "code_hash": {
7368 + "name": "code_hash",
7369 + "type": "text",
7370 + "primaryKey": false,
7371 + "notNull": true
7372 + },
7373 + "expires_at": {
7374 + "name": "expires_at",
7375 + "type": "timestamp with time zone",
7376 + "primaryKey": false,
7377 + "notNull": true
7378 + },
7379 + "attempts": {
7380 + "name": "attempts",
7381 + "type": "integer",
7382 + "primaryKey": false,
7383 + "notNull": true,
7384 + "default": 0
7385 + },
7386 + "consumed_at": {
7387 + "name": "consumed_at",
7388 + "type": "timestamp with time zone",
7389 + "primaryKey": false,
7390 + "notNull": false
7391 + },
7392 + "payload": {
7393 + "name": "payload",
7394 + "type": "jsonb",
7395 + "primaryKey": false,
7396 + "notNull": true,
7397 + "default": "'{}'::jsonb"
7398 + },
7399 + "created_at": {
7400 + "name": "created_at",
7401 + "type": "timestamp with time zone",
7402 + "primaryKey": false,
7403 + "notNull": true,
7404 + "default": "now()"
7405 + }
7406 + },
7407 + "indexes": {
7408 + "auth_codes_lookup_idx": {
7409 + "name": "auth_codes_lookup_idx",
7410 + "columns": [
7411 + {
7412 + "expression": "email",
7413 + "isExpression": false,
7414 + "asc": true,
7415 + "nulls": "last"
7416 + },
7417 + {
7418 + "expression": "purpose",
7419 + "isExpression": false,
7420 + "asc": true,
7421 + "nulls": "last"
7422 + },
7423 + {
7424 + "expression": "created_at",
7425 + "isExpression": false,
7426 + "asc": true,
7427 + "nulls": "last"
7428 + }
7429 + ],
7430 + "isUnique": false,
7431 + "concurrently": false,
7432 + "method": "btree",
7433 + "with": {}
7434 + }
7435 + },
7436 + "foreignKeys": {},
7437 + "compositePrimaryKeys": {},
7438 + "uniqueConstraints": {},
7439 + "policies": {},
7440 + "checkConstraints": {},
7441 + "isRLSEnabled": false
7442 + },
7443 + "public.login_events": {
7444 + "name": "login_events",
7445 + "schema": "",
7446 + "columns": {
7447 + "id": {
7448 + "name": "id",
7449 + "type": "text",
7450 + "primaryKey": true,
7451 + "notNull": true
7452 + },
7453 + "user_id": {
7454 + "name": "user_id",
7455 + "type": "text",
7456 + "primaryKey": false,
7457 + "notNull": false
7458 + },
7459 + "email": {
7460 + "name": "email",
7461 + "type": "text",
7462 + "primaryKey": false,
7463 + "notNull": false
7464 + },
7465 + "ip": {
7466 + "name": "ip",
7467 + "type": "text",
7468 + "primaryKey": false,
7469 + "notNull": false
7470 + },
7471 + "user_agent": {
7472 + "name": "user_agent",
7473 + "type": "text",
7474 + "primaryKey": false,
7475 + "notNull": false
7476 + },
7477 + "outcome": {
7478 + "name": "outcome",
7479 + "type": "text",
7480 + "primaryKey": false,
7481 + "notNull": true
7482 + },
7483 + "method": {
7484 + "name": "method",
7485 + "type": "text",
7486 + "primaryKey": false,
7487 + "notNull": false
7488 + },
7489 + "created_at": {
7490 + "name": "created_at",
7491 + "type": "timestamp with time zone",
7492 + "primaryKey": false,
7493 + "notNull": true,
7494 + "default": "now()"
7495 + }
7496 + },
7497 + "indexes": {
7498 + "login_events_user_idx": {
7499 + "name": "login_events_user_idx",
7500 + "columns": [
7501 + {
7502 + "expression": "user_id",
7503 + "isExpression": false,
7504 + "asc": true,
7505 + "nulls": "last"
7506 + },
7507 + {
7508 + "expression": "created_at",
7509 + "isExpression": false,
7510 + "asc": true,
7511 + "nulls": "last"
7512 + }
7513 + ],
7514 + "isUnique": false,
7515 + "concurrently": false,
7516 + "method": "btree",
7517 + "with": {}
7518 + }
7519 + },
7520 + "foreignKeys": {},
7521 + "compositePrimaryKeys": {},
7522 + "uniqueConstraints": {},
7523 + "policies": {},
7524 + "checkConstraints": {},
7525 + "isRLSEnabled": false
7526 + },
7527 + "public.notifications": {
7528 + "name": "notifications",
7529 + "schema": "",
7530 + "columns": {
7531 + "id": {
7532 + "name": "id",
7533 + "type": "text",
7534 + "primaryKey": true,
7535 + "notNull": true
7536 + },
7537 + "user_id": {
7538 + "name": "user_id",
7539 + "type": "text",
7540 + "primaryKey": false,
7541 + "notNull": true
7542 + },
7543 + "kind": {
7544 + "name": "kind",
7545 + "type": "text",
7546 + "primaryKey": false,
7547 + "notNull": true
7548 + },
7549 + "title": {
7550 + "name": "title",
7551 + "type": "text",
7552 + "primaryKey": false,
7553 + "notNull": true
7554 + },
7555 + "body": {
7556 + "name": "body",
7557 + "type": "text",
7558 + "primaryKey": false,
7559 + "notNull": false
7560 + },
7561 + "href": {
7562 + "name": "href",
7563 + "type": "text",
7564 + "primaryKey": false,
7565 + "notNull": false
7566 + },
7567 + "payload": {
7568 + "name": "payload",
7569 + "type": "jsonb",
7570 + "primaryKey": false,
7571 + "notNull": true,
7572 + "default": "'{}'::jsonb"
7573 + },
7574 + "read_at": {
7575 + "name": "read_at",
7576 + "type": "timestamp with time zone",
7577 + "primaryKey": false,
7578 + "notNull": false
7579 + },
7580 + "emailed_at": {
7581 + "name": "emailed_at",
7582 + "type": "timestamp with time zone",
7583 + "primaryKey": false,
7584 + "notNull": false
7585 + },
7586 + "created_at": {
7587 + "name": "created_at",
7588 + "type": "timestamp with time zone",
7589 + "primaryKey": false,
7590 + "notNull": true,
7591 + "default": "now()"
7592 + }
7593 + },
7594 + "indexes": {
7595 + "notifications_user_idx": {
7596 + "name": "notifications_user_idx",
7597 + "columns": [
7598 + {
7599 + "expression": "user_id",
7600 + "isExpression": false,
7601 + "asc": true,
7602 + "nulls": "last"
7603 + },
7604 + {
7605 + "expression": "created_at",
7606 + "isExpression": false,
7607 + "asc": true,
7608 + "nulls": "last"
7609 + }
7610 + ],
7611 + "isUnique": false,
7612 + "concurrently": false,
7613 + "method": "btree",
7614 + "with": {}
7615 + },
7616 + "notifications_unread_idx": {
7617 + "name": "notifications_unread_idx",
7618 + "columns": [
7619 + {
7620 + "expression": "user_id",
7621 + "isExpression": false,
7622 + "asc": true,
7623 + "nulls": "last"
7624 + },
7625 + {
7626 + "expression": "read_at",
7627 + "isExpression": false,
7628 + "asc": true,
7629 + "nulls": "last"
7630 + }
7631 + ],
7632 + "isUnique": false,
7633 + "concurrently": false,
7634 + "method": "btree",
7635 + "with": {}
7636 + }
7637 + },
7638 + "foreignKeys": {},
7639 + "compositePrimaryKeys": {},
7640 + "uniqueConstraints": {},
7641 + "policies": {},
7642 + "checkConstraints": {},
7643 + "isRLSEnabled": false
7644 + },
7645 + "public.price_targets": {
7646 + "name": "price_targets",
7647 + "schema": "",
7648 + "columns": {
7649 + "id": {
7650 + "name": "id",
7651 + "type": "text",
7652 + "primaryKey": true,
7653 + "notNull": true
7654 + },
7655 + "user_id": {
7656 + "name": "user_id",
7657 + "type": "text",
7658 + "primaryKey": false,
7659 + "notNull": true
7660 + },
7661 + "asset_id": {
7662 + "name": "asset_id",
7663 + "type": "text",
7664 + "primaryKey": false,
7665 + "notNull": true
7666 + },
7667 + "variant_id": {
7668 + "name": "variant_id",
7669 + "type": "text",
7670 + "primaryKey": false,
7671 + "notNull": false
7672 + },
7673 + "direction": {
7674 + "name": "direction",
7675 + "type": "text",
7676 + "primaryKey": false,
7677 + "notNull": true,
7678 + "default": "'above'"
7679 + },
7680 + "target_usd": {
7681 + "name": "target_usd",
7682 + "type": "numeric(18, 4)",
7683 + "primaryKey": false,
7684 + "notNull": true
7685 + },
7686 + "baseline_usd": {
7687 + "name": "baseline_usd",
7688 + "type": "numeric(18, 4)",
7689 + "primaryKey": false,
7690 + "notNull": false
7691 + },
7692 + "note": {
7693 + "name": "note",
7694 + "type": "text",
7695 + "primaryKey": false,
7696 + "notNull": false
7697 + },
7698 + "hit_at": {
7699 + "name": "hit_at",
7700 + "type": "timestamp with time zone",
7701 + "primaryKey": false,
7702 + "notNull": false
7703 + },
7704 + "notified_at": {
7705 + "name": "notified_at",
7706 + "type": "timestamp with time zone",
7707 + "primaryKey": false,
7708 + "notNull": false
7709 + },
7710 + "created_at": {
7711 + "name": "created_at",
7712 + "type": "timestamp with time zone",
7713 + "primaryKey": false,
7714 + "notNull": true,
7715 + "default": "now()"
7716 + }
7717 + },
7718 + "indexes": {
7719 + "price_targets_user_idx": {
7720 + "name": "price_targets_user_idx",
7721 + "columns": [
7722 + {
7723 + "expression": "user_id",
7724 + "isExpression": false,
7725 + "asc": true,
7726 + "nulls": "last"
7727 + }
7728 + ],
7729 + "isUnique": false,
7730 + "concurrently": false,
7731 + "method": "btree",
7732 + "with": {}
7733 + },
7734 + "price_targets_asset_idx": {
7735 + "name": "price_targets_asset_idx",
7736 + "columns": [
7737 + {
7738 + "expression": "asset_id",
7739 + "isExpression": false,
7740 + "asc": true,
7741 + "nulls": "last"
7742 + },
7743 + {
7744 + "expression": "hit_at",
7745 + "isExpression": false,
7746 + "asc": true,
7747 + "nulls": "last"
7748 + }
7749 + ],
7750 + "isUnique": false,
7751 + "concurrently": false,
7752 + "method": "btree",
7753 + "with": {}
7754 + }
7755 + },
7756 + "foreignKeys": {},
7757 + "compositePrimaryKeys": {},
7758 + "uniqueConstraints": {},
7759 + "policies": {},
7760 + "checkConstraints": {},
7761 + "isRLSEnabled": false
7762 + },
7763 + "public.rate_limits": {
7764 + "name": "rate_limits",
7765 + "schema": "",
7766 + "columns": {
7767 + "key": {
7768 + "name": "key",
7769 + "type": "text",
7770 + "primaryKey": true,
7771 + "notNull": true
7772 + },
7773 + "count": {
7774 + "name": "count",
7775 + "type": "integer",
7776 + "primaryKey": false,
7777 + "notNull": true,
7778 + "default": 0
7779 + },
7780 + "reset_at": {
7781 + "name": "reset_at",
7782 + "type": "timestamp with time zone",
7783 + "primaryKey": false,
7784 + "notNull": true
7785 + }
7786 + },
7787 + "indexes": {},
7788 + "foreignKeys": {},
7789 + "compositePrimaryKeys": {},
7790 + "uniqueConstraints": {},
7791 + "policies": {},
7792 + "checkConstraints": {},
7793 + "isRLSEnabled": false
7794 + },
7795 + "public.recovery_codes": {
7796 + "name": "recovery_codes",
7797 + "schema": "",
7798 + "columns": {
7799 + "id": {
7800 + "name": "id",
7801 + "type": "text",
7802 + "primaryKey": true,
7803 + "notNull": true
7804 + },
7805 + "user_id": {
7806 + "name": "user_id",
7807 + "type": "text",
7808 + "primaryKey": false,
7809 + "notNull": true
7810 + },
7811 + "code_hash": {
7812 + "name": "code_hash",
7813 + "type": "text",
7814 + "primaryKey": false,
7815 + "notNull": true
7816 + },
7817 + "used_at": {
7818 + "name": "used_at",
7819 + "type": "timestamp with time zone",
7820 + "primaryKey": false,
7821 + "notNull": false
7822 + },
7823 + "created_at": {
7824 + "name": "created_at",
7825 + "type": "timestamp with time zone",
7826 + "primaryKey": false,
7827 + "notNull": true,
7828 + "default": "now()"
7829 + }
7830 + },
7831 + "indexes": {
7832 + "recovery_codes_user_idx": {
7833 + "name": "recovery_codes_user_idx",
7834 + "columns": [
7835 + {
7836 + "expression": "user_id",
7837 + "isExpression": false,
7838 + "asc": true,
7839 + "nulls": "last"
7840 + }
7841 + ],
7842 + "isUnique": false,
7843 + "concurrently": false,
7844 + "method": "btree",
7845 + "with": {}
7846 + }
7847 + },
7848 + "foreignKeys": {},
7849 + "compositePrimaryKeys": {},
7850 + "uniqueConstraints": {},
7851 + "policies": {},
7852 + "checkConstraints": {},
7853 + "isRLSEnabled": false
7854 + },
7855 + "public.saved_searches": {
7856 + "name": "saved_searches",
7857 + "schema": "",
7858 + "columns": {
7859 + "id": {
7860 + "name": "id",
7861 + "type": "text",
7862 + "primaryKey": true,
7863 + "notNull": true
7864 + },
7865 + "user_id": {
7866 + "name": "user_id",
7867 + "type": "text",
7868 + "primaryKey": false,
7869 + "notNull": true
7870 + },
7871 + "name": {
7872 + "name": "name",
7873 + "type": "text",
7874 + "primaryKey": false,
7875 + "notNull": true
7876 + },
7877 + "url": {
7878 + "name": "url",
7879 + "type": "text",
7880 + "primaryKey": false,
7881 + "notNull": true
7882 + },
7883 + "params": {
7884 + "name": "params",
7885 + "type": "jsonb",
7886 + "primaryKey": false,
7887 + "notNull": true,
7888 + "default": "'{}'::jsonb"
7889 + },
7890 + "notify": {
7891 + "name": "notify",
7892 + "type": "boolean",
7893 + "primaryKey": false,
7894 + "notNull": true,
7895 + "default": false
7896 + },
7897 + "last_run_at": {
7898 + "name": "last_run_at",
7899 + "type": "timestamp with time zone",
7900 + "primaryKey": false,
7901 + "notNull": false
7902 + },
7903 + "last_count": {
7904 + "name": "last_count",
7905 + "type": "integer",
7906 + "primaryKey": false,
7907 + "notNull": false
7908 + },
7909 + "created_at": {
7910 + "name": "created_at",
7911 + "type": "timestamp with time zone",
7912 + "primaryKey": false,
7913 + "notNull": true,
7914 + "default": "now()"
7915 + }
7916 + },
7917 + "indexes": {
7918 + "saved_searches_user_idx": {
7919 + "name": "saved_searches_user_idx",
7920 + "columns": [
7921 + {
7922 + "expression": "user_id",
7923 + "isExpression": false,
7924 + "asc": true,
7925 + "nulls": "last"
7926 + }
7927 + ],
7928 + "isUnique": false,
7929 + "concurrently": false,
7930 + "method": "btree",
7931 + "with": {}
7932 + }
7933 + },
7934 + "foreignKeys": {},
7935 + "compositePrimaryKeys": {},
7936 + "uniqueConstraints": {},
7937 + "policies": {},
7938 + "checkConstraints": {},
7939 + "isRLSEnabled": false
7940 + },
7941 + "public.trusted_devices": {
7942 + "name": "trusted_devices",
7943 + "schema": "",
7944 + "columns": {
7945 + "id": {
7946 + "name": "id",
7947 + "type": "text",
7948 + "primaryKey": true,
7949 + "notNull": true
7950 + },
7951 + "user_id": {
7952 + "name": "user_id",
7953 + "type": "text",
7954 + "primaryKey": false,
7955 + "notNull": true
7956 + },
7957 + "token_hash": {
7958 + "name": "token_hash",
7959 + "type": "text",
7960 + "primaryKey": false,
7961 + "notNull": true
7962 + },
7963 + "label": {
7964 + "name": "label",
7965 + "type": "text",
7966 + "primaryKey": false,
7967 + "notNull": false
7968 + },
7969 + "user_agent": {
7970 + "name": "user_agent",
7971 + "type": "text",
7972 + "primaryKey": false,
7973 + "notNull": false
7974 + },
7975 + "ip": {
7976 + "name": "ip",
7977 + "type": "text",
7978 + "primaryKey": false,
7979 + "notNull": false
7980 + },
7981 + "created_at": {
7982 + "name": "created_at",
7983 + "type": "timestamp with time zone",
7984 + "primaryKey": false,
7985 + "notNull": true,
7986 + "default": "now()"
7987 + },
7988 + "last_used_at": {
7989 + "name": "last_used_at",
7990 + "type": "timestamp with time zone",
7991 + "primaryKey": false,
7992 + "notNull": false
7993 + },
7994 + "expires_at": {
7995 + "name": "expires_at",
7996 + "type": "timestamp with time zone",
7997 + "primaryKey": false,
7998 + "notNull": true
7999 + },
8000 + "revoked_at": {
8001 + "name": "revoked_at",
8002 + "type": "timestamp with time zone",
8003 + "primaryKey": false,
8004 + "notNull": false
8005 + }
8006 + },
8007 + "indexes": {
8008 + "trusted_devices_user_idx": {
8009 + "name": "trusted_devices_user_idx",
8010 + "columns": [
8011 + {
8012 + "expression": "user_id",
8013 + "isExpression": false,
8014 + "asc": true,
8015 + "nulls": "last"
8016 + }
8017 + ],
8018 + "isUnique": false,
8019 + "concurrently": false,
8020 + "method": "btree",
8021 + "with": {}
8022 + },
8023 + "trusted_devices_token_uq": {
8024 + "name": "trusted_devices_token_uq",
8025 + "columns": [
8026 + {
8027 + "expression": "token_hash",
8028 + "isExpression": false,
8029 + "asc": true,
8030 + "nulls": "last"
8031 + }
8032 + ],
8033 + "isUnique": true,
8034 + "concurrently": false,
8035 + "method": "btree",
8036 + "with": {}
8037 + }
8038 + },
8039 + "foreignKeys": {},
8040 + "compositePrimaryKeys": {},
8041 + "uniqueConstraints": {},
8042 + "policies": {},
8043 + "checkConstraints": {},
8044 + "isRLSEnabled": false
8045 + },
8046 + "public.uploads": {
8047 + "name": "uploads",
8048 + "schema": "",
8049 + "columns": {
8050 + "id": {
8051 + "name": "id",
8052 + "type": "text",
8053 + "primaryKey": true,
8054 + "notNull": true
8055 + },
8056 + "user_id": {
8057 + "name": "user_id",
8058 + "type": "text",
8059 + "primaryKey": false,
8060 + "notNull": true
8061 + },
8062 + "kind": {
8063 + "name": "kind",
8064 + "type": "text",
8065 + "primaryKey": false,
8066 + "notNull": true
8067 + },
8068 + "path": {
8069 + "name": "path",
8070 + "type": "text",
8071 + "primaryKey": false,
8072 + "notNull": true
8073 + },
8074 + "mime": {
8075 + "name": "mime",
8076 + "type": "text",
8077 + "primaryKey": false,
8078 + "notNull": true
8079 + },
8080 + "bytes": {
8081 + "name": "bytes",
8082 + "type": "integer",
8083 + "primaryKey": false,
8084 + "notNull": true
8085 + },
8086 + "width": {
8087 + "name": "width",
8088 + "type": "integer",
8089 + "primaryKey": false,
8090 + "notNull": false
8091 + },
8092 + "height": {
8093 + "name": "height",
8094 + "type": "integer",
8095 + "primaryKey": false,
8096 + "notNull": false
8097 + },
8098 + "created_at": {
8099 + "name": "created_at",
8100 + "type": "timestamp with time zone",
8101 + "primaryKey": false,
8102 + "notNull": true,
8103 + "default": "now()"
8104 + }
8105 + },
8106 + "indexes": {
8107 + "uploads_user_idx": {
8108 + "name": "uploads_user_idx",
8109 + "columns": [
8110 + {
8111 + "expression": "user_id",
8112 + "isExpression": false,
8113 + "asc": true,
8114 + "nulls": "last"
8115 + }
8116 + ],
8117 + "isUnique": false,
8118 + "concurrently": false,
8119 + "method": "btree",
8120 + "with": {}
8121 + }
8122 + },
8123 + "foreignKeys": {},
8124 + "compositePrimaryKeys": {},
8125 + "uniqueConstraints": {},
8126 + "policies": {},
8127 + "checkConstraints": {},
8128 + "isRLSEnabled": false
8129 + },
8130 + "public.user_badges": {
8131 + "name": "user_badges",
8132 + "schema": "",
8133 + "columns": {
8134 + "user_id": {
8135 + "name": "user_id",
8136 + "type": "text",
8137 + "primaryKey": false,
8138 + "notNull": true
8139 + },
8140 + "badge": {
8141 + "name": "badge",
8142 + "type": "text",
8143 + "primaryKey": false,
8144 + "notNull": true
8145 + },
8146 + "evidence": {
8147 + "name": "evidence",
8148 + "type": "jsonb",
8149 + "primaryKey": false,
8150 + "notNull": true,
8151 + "default": "'{}'::jsonb"
8152 + },
8153 + "created_at": {
8154 + "name": "created_at",
8155 + "type": "timestamp with time zone",
8156 + "primaryKey": false,
8157 + "notNull": true,
8158 + "default": "now()"
8159 + }
8160 + },
8161 + "indexes": {
8162 + "user_badges_uq": {
8163 + "name": "user_badges_uq",
8164 + "columns": [
8165 + {
8166 + "expression": "user_id",
8167 + "isExpression": false,
8168 + "asc": true,
8169 + "nulls": "last"
8170 + },
8171 + {
8172 + "expression": "badge",
8173 + "isExpression": false,
8174 + "asc": true,
8175 + "nulls": "last"
8176 + }
8177 + ],
8178 + "isUnique": true,
8179 + "concurrently": false,
8180 + "method": "btree",
8181 + "with": {}
8182 + }
8183 + },
8184 + "foreignKeys": {},
8185 + "compositePrimaryKeys": {},
8186 + "uniqueConstraints": {},
8187 + "policies": {},
8188 + "checkConstraints": {},
8189 + "isRLSEnabled": false
8190 + }
8191 + },
8192 + "enums": {},
8193 + "schemas": {},
8194 + "sequences": {},
8195 + "roles": {},
8196 + "policies": {},
8197 + "views": {},
8198 + "_meta": {
8199 + "columns": {},
8200 + "schemas": {},
8201 + "tables": {}
8202 + }
8203 +}
\ No newline at end of file
modified packages/database/migrations/meta/_journal.json +7 −0
@@ -22,6 +22,13 @@
22 22 "when": 1788763418962,
23 23 "tag": "0002_broken_the_phantom",
24 24 "breakpoints": true
25 + },
26 + {
27 + "idx": 3,
28 + "version": "7",
29 + "when": 1788764880950,
30 + "tag": "0003_curved_sage",
31 + "breakpoints": true
25 32 }
26 33 ]
27 34 }
\ No newline at end of file
modified packages/database/src/schema/assets.ts +9 −1
@@ -171,9 +171,17 @@ export const images = pgTable(
171 171 phash: text('phash'),
172 172 embedding: vector('embedding', { dimensions: 512 }),
173 173 attribution: text('attribution'),
174 + /** unchecked | ok | dead | blocked | error (§113 image pipeline) */
175 + status: text('status').notNull().default('unchecked'),
176 + checkedAt: ts('checked_at'),
177 + bytes: integer('bytes'),
178 + contentType: text('content_type'),
179 + /** sha1(url): key of the on-disk cache under RI_DATA_DIR/images */
180 + cacheKey: text('cache_key'),
181 + error: text('error'),
174 182 createdAt: createdAt(),
175 183 },
176 − (t) => [index('images_asset_idx').on(t.assetId), index('images_phash_idx').on(t.phash), uniqueIndex('images_url_uq').on(t.url)],
184 + (t) => [index('images_asset_idx').on(t.assetId), index('images_phash_idx').on(t.phash), uniqueIndex('images_url_uq').on(t.url), index('images_cache_key_idx').on(t.cacheKey), index('images_status_idx').on(t.status, t.checkedAt)],
177 185 );
178 186
179 187 /** Text embeddings for hybrid search / entity resolution (§112, §138). */
modified pnpm-lock.yaml +7 −3
@@ -123,6 +123,9 @@ importers:
123 123 server-only:
124 124 specifier: ^0.0.1
125 125 version: 0.0.1
126 + sharp:
127 + specifier: ^0.35.0
128 + version: 0.35.4(@types/node@24.13.3)
126 129 zod:
127 130 specifier: ^4.0.0
128 131 version: 4.5.4
@@ -416,6 +419,9 @@ importers:
416 419 postgres:
417 420 specifier: ^3.4.7
418 421 version: 3.4.9
422 + sharp:
423 + specifier: ^0.35.0
424 + version: 0.35.4(@types/node@24.13.3)
419 425 zod:
420 426 specifier: ^4.0.0
421 427 version: 4.5.4
@@ -4364,8 +4370,7 @@ snapshots:
4364 4370
4365 4371 '@humanwhocodes/retry@0.4.3': {}
4366 4372
4367 − '@img/colour@1.1.0':
4368 − optional: true
4373 + '@img/colour@1.1.0': {}
4369 4374
4370 4375 '@img/sharp-darwin-arm64@0.35.4':
4371 4376 optionalDependencies:
@@ -6766,7 +6771,6 @@ snapshots:
6766 6771 '@img/sharp-win32-ia32': 0.35.4
6767 6772 '@img/sharp-win32-x64': 0.35.4
6768 6773 '@types/node': 24.13.3
6769 − optional: true
6770 6774
6771 6775 shebang-command@2.0.0:
6772 6776 dependencies:
modified workers/cli.ts +6 −0
@@ -25,6 +25,7 @@ import { syncFx } from './fx.ts';
25 25 import { syncBenchmarks } from './benchmarks.ts';
26 26 import { computeHealth } from './health.ts';
27 27 import { expireListings } from './listings-expire.ts';
28 +import { imageStats, processImages } from './image-processing/index.ts';
28 29
29 30 const [cmd = 'help', ...rest] = process.argv.slice(2);
30 31 const flags: Record<string, string | boolean> = {};
@@ -125,6 +126,11 @@ async function main() {
125 126 case 'stats':
126 127 print(await stats());
127 128 break;
129 + case 'images': {
130 + const r = await processImages({ limit: num('limit', 2000), recheck: Boolean(flags.recheck) });
131 + print({ ...r, totals: await imageStats() });
132 + break;
133 + }
128 134 case 'run-all': {
129 135 const limit = num('limit', 200);
130 136 const [fx] = (await db().execute(sql`select count(*)::int as n from fx_rates`)) as unknown as Array<{ n: number }>;
added workers/image-processing/core.ts +299 −0
@@ -0,0 +1,299 @@
1 +/**
2 + * Image cache core (§113 basics, §166 delivery). Node-only: shared by the `/img` proxy route
3 + * (apps/web, via src/lib/images-core.ts re-export + experimental.externalDir) and the
4 + * image-processing worker. Lives in workers/ so it is an ES module for tsc. No Next.js imports here.
5 + *
6 + * Why a self-hosted cache: Next's optimizer fetches with Node's default `User-Agent: node`, which
7 + * Scryfall (62k Magic images) rejects with 400; other hosts hot-link-protect or rate-limit. We fetch
8 + * once with honest, host-appropriate headers, keep the original on disk and serve resized WebP.
9 + */
10 +import { createHash } from 'node:crypto';
11 +import { existsSync, mkdirSync } from 'node:fs';
12 +import { readFile, rename, stat, writeFile } from 'node:fs/promises';
13 +import path from 'node:path';
14 +import sharp, { type Metadata } from 'sharp';
15 +
16 +export const IMAGE_WIDTHS = [96, 192, 384, 768, 1200] as const;
17 +export type ImageWidth = (typeof IMAGE_WIDTHS)[number];
18 +export const MAX_BYTES = 8 * 1024 * 1024;
19 +export const FETCH_TIMEOUT_MS = 20_000;
20 +const NEGATIVE_TTL_MS = 24 * 3600_000;
21 +const USER_AGENT = 'RareIndexImageCache/1.0 (+https://www.rareindex.io/about; caches product images with attribution; contact data@rareindex.io)';
22 +const ACCEPT = 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8';
23 +
24 +/** Per-host request rules. `referer: 'self'` sends the host's own origin (hot-link protection). */
25 +interface HostRule {
26 + referer?: 'self' | string;
27 + headers?: Record<string, string>;
28 + maxConcurrent?: number;
29 + /** minimum ms between requests to the host */
30 + minIntervalMs?: number;
31 +}
32 +const HOST_RULES: Array<[RegExp, HostRule]> = [
33 + [/(^|\.)scryfall\.(io|com)$/, { maxConcurrent: 4, minIntervalMs: 60 }],
34 + [/^storage\.googleapis\.com$/, { referer: 'https://www.pricecharting.com/', maxConcurrent: 6 }],
35 + [/(^|\.)pricecharting\.com$/, { referer: 'self', maxConcurrent: 4 }],
36 + [/(^|\.)chrono24\.(com|de|fr|ch)$/, { referer: 'https://www.chrono24.com/', maxConcurrent: 3, minIntervalMs: 200 }],
37 + [/(^|\.)goldin\.co$/, { referer: 'https://goldin.co/', maxConcurrent: 3 }],
38 + [/(^|\.)comicconnect\.com$/, { referer: 'self', maxConcurrent: 3 }],
39 + [/(^|\.)phillips\.com$/, { referer: 'https://www.phillips.com/', maxConcurrent: 4 }],
40 + [/(^|\.)brickeconomy\.com$/, { referer: 'self', maxConcurrent: 2, minIntervalMs: 300 }],
41 + [/(^|\.)ygoprodeck\.com$/, { maxConcurrent: 4, minIntervalMs: 50 }],
42 + [/(^|\.)pokemontcg\.io$/, { maxConcurrent: 4 }],
43 + [/(^|\.)tcgdex\.net$/, { maxConcurrent: 4 }],
44 + [/(^|\.)lorcast\.io$/, { maxConcurrent: 4 }],
45 + [/(^|\.)cloudfront\.net$/, { maxConcurrent: 6 }],
46 + [/(^|\.)catawiki\.(com|nl)$|(^|\.)assets\.catawiki\.nl$/, { referer: 'https://www.catawiki.com/', maxConcurrent: 3 }],
47 + [/(^|\.)sothebys\.com$|(^|\.)christies\.com$|(^|\.)bonhams\.com$/, { referer: 'self', maxConcurrent: 3 }],
48 + [/(^|\.)bringatrailer\.com$/, { referer: 'self', maxConcurrent: 3 }],
49 + [/(^|\.)ebayimg\.com$/, { maxConcurrent: 6 }],
50 +];
51 +
52 +export function hostRule(host: string): HostRule {
53 + const h = host.toLowerCase();
54 + for (const [re, rule] of HOST_RULES) if (re.test(h)) return rule;
55 + return { maxConcurrent: 3 };
56 +}
57 +
58 +export function requestHeadersFor(url: URL): Record<string, string> {
59 + const rule = hostRule(url.hostname);
60 + const headers: Record<string, string> = { 'user-agent': USER_AGENT, accept: ACCEPT, 'accept-language': 'en-US,en;q=0.8', ...(rule.headers ?? {}) };
61 + if (rule.referer === 'self') headers.referer = `${url.protocol}//${url.hostname}/`;
62 + else if (rule.referer) headers.referer = rule.referer;
63 + return headers;
64 +}
65 +
66 +export function imageKey(url: string): string {
67 + return createHash('sha1').update(url).digest('hex');
68 +}
69 +
70 +/** Resolve RI_DATA_DIR; a relative value is anchored at the monorepo root so web and workers share one cache. */
71 +export function dataDir(): string {
72 + const raw = process.env.RI_DATA_DIR ?? './data';
73 + if (path.isAbsolute(raw)) return raw;
74 + let dir = process.cwd();
75 + for (let i = 0; i < 5; i++) {
76 + if (existsSync(path.join(dir, 'pnpm-workspace.yaml'))) return path.resolve(dir, raw);
77 + const parent = path.dirname(dir);
78 + if (parent === dir) break;
79 + dir = parent;
80 + }
81 + return path.resolve(process.cwd(), raw);
82 +}
83 +
84 +export function cacheRoot(): string {
85 + return path.join(dataDir(), 'images');
86 +}
87 +export function origPath(key: string, ext: string): string {
88 + return path.join(cacheRoot(), 'orig', key.slice(0, 2), `${key}.${ext}`);
89 +}
90 +export function variantPath(key: string, width: number, fmt: 'webp' | 'avif'): string {
91 + return path.join(cacheRoot(), `w${width}`, key.slice(0, 2), `${key}.${fmt}`);
92 +}
93 +
94 +export function nearestWidth(w: unknown): ImageWidth {
95 + const n = Number(w);
96 + if (!Number.isFinite(n) || n <= 0) return 384;
97 + for (const cand of IMAGE_WIDTHS) if (n <= cand) return cand;
98 + return 1200;
99 +}
100 +
101 +// ---------- host politeness ----------
102 +const inflight = new Map<string, number>();
103 +const lastAt = new Map<string, number>();
104 +async function acquire(host: string): Promise<() => void> {
105 + const rule = hostRule(host);
106 + const max = rule.maxConcurrent ?? 3;
107 + while ((inflight.get(host) ?? 0) >= max) await new Promise((r) => setTimeout(r, 25));
108 + inflight.set(host, (inflight.get(host) ?? 0) + 1);
109 + const wait = (lastAt.get(host) ?? 0) + (rule.minIntervalMs ?? 0) - Date.now();
110 + if (wait > 0) await new Promise((r) => setTimeout(r, wait));
111 + lastAt.set(host, Date.now());
112 + return () => inflight.set(host, Math.max(0, (inflight.get(host) ?? 1) - 1));
113 +}
114 +
115 +// ---------- negative cache (in-process; the worker also persists status in the DB) ----------
116 +const negative = new Map<string, { until: number; reason: string }>();
117 +export function negativeFor(url: string): string | null {
118 + const n = negative.get(url);
119 + if (!n) return null;
120 + if (n.until < Date.now()) {
121 + negative.delete(url);
122 + return null;
123 + }
124 + return n.reason;
125 +}
126 +export function markNegative(url: string, reason: string, ttlMs = NEGATIVE_TTL_MS): void {
127 + negative.set(url, { until: Date.now() + ttlMs, reason });
128 + if (negative.size > 50_000) negative.delete(negative.keys().next().value!);
129 +}
130 +
131 +export interface OriginalInfo {
132 + key: string;
133 + path: string;
134 + ext: string;
135 + contentType: string;
136 + bytes: number;
137 + width: number | null;
138 + height: number | null;
139 + fromCache: boolean;
140 +}
141 +export type FetchOutcome = { ok: true; info: OriginalInfo } | { ok: false; status: 'dead' | 'blocked' | 'error'; reason: string; httpStatus: number | null };
142 +
143 +const EXT_BY_FORMAT: Record<string, string> = { jpeg: 'jpg', jpg: 'jpg', png: 'png', webp: 'webp', avif: 'avif', gif: 'gif', tiff: 'tif', svg: 'svg', heif: 'heic' };
144 +const CT_BY_EXT: Record<string, string> = { jpg: 'image/jpeg', png: 'image/png', webp: 'image/webp', avif: 'image/avif', gif: 'image/gif', tif: 'image/tiff', svg: 'image/svg+xml', heic: 'image/heic' };
145 +
146 +const originalInflight = new Map<string, Promise<FetchOutcome>>();
147 +
148 +/** Find an already cached original for a key (any extension). */
149 +export async function findOriginal(key: string): Promise<OriginalInfo | null> {
150 + for (const ext of Object.keys(CT_BY_EXT)) {
151 + const p = origPath(key, ext);
152 + try {
153 + const s = await stat(p);
154 + return { key, path: p, ext, contentType: CT_BY_EXT[ext]!, bytes: s.size, width: null, height: null, fromCache: true };
155 + } catch {
156 + /* next */
157 + }
158 + }
159 + return null;
160 +}
161 +
162 +/** Fetch + validate + store the original image; idempotent and de-duplicated per URL. */
163 +export async function ensureOriginal(url: string): Promise<FetchOutcome> {
164 + const key = imageKey(url);
165 + const cached = await findOriginal(key);
166 + if (cached) return { ok: true, info: cached };
167 + const neg = negativeFor(url);
168 + if (neg) return { ok: false, status: neg.startsWith('blocked') ? 'blocked' : 'dead', reason: neg, httpStatus: null };
169 + const existing = originalInflight.get(url);
170 + if (existing) return existing;
171 + const p = fetchOriginal(url, key).finally(() => originalInflight.delete(url));
172 + originalInflight.set(url, p);
173 + return p;
174 +}
175 +
176 +async function fetchOriginal(url: string, key: string): Promise<FetchOutcome> {
177 + let u: URL;
178 + try {
179 + u = new URL(url);
180 + if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error('protocol');
181 + } catch {
182 + markNegative(url, 'dead: invalid url');
183 + return { ok: false, status: 'dead', reason: 'invalid url', httpStatus: null };
184 + }
185 + const release = await acquire(u.hostname);
186 + const ctrl = new AbortController();
187 + const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
188 + try {
189 + const res = await fetch(u, { headers: requestHeadersFor(u), signal: ctrl.signal, redirect: 'follow' });
190 + if (res.status === 404 || res.status === 410) {
191 + markNegative(url, `dead: http ${res.status}`);
192 + return { ok: false, status: 'dead', reason: `http ${res.status}`, httpStatus: res.status };
193 + }
194 + if (res.status === 401 || res.status === 403 || res.status === 429 || res.status === 400) {
195 + markNegative(url, `blocked: http ${res.status}`, res.status === 429 ? 3600_000 : NEGATIVE_TTL_MS);
196 + return { ok: false, status: 'blocked', reason: `http ${res.status}`, httpStatus: res.status };
197 + }
198 + if (!res.ok) {
199 + markNegative(url, `error: http ${res.status}`, 3600_000);
200 + return { ok: false, status: 'error', reason: `http ${res.status}`, httpStatus: res.status };
201 + }
202 + const len = Number(res.headers.get('content-length') ?? 0);
203 + if (len > MAX_BYTES) {
204 + markNegative(url, 'dead: too large');
205 + return { ok: false, status: 'dead', reason: `too large (${len} bytes)`, httpStatus: res.status };
206 + }
207 + const buf = Buffer.from(await res.arrayBuffer());
208 + if (buf.byteLength === 0 || buf.byteLength > MAX_BYTES) {
209 + markNegative(url, 'dead: empty or too large');
210 + return { ok: false, status: 'dead', reason: `size ${buf.byteLength}`, httpStatus: res.status };
211 + }
212 + // Validate by decoding metadata (also guards against HTML error pages served as 200).
213 + let meta: Metadata;
214 + try {
215 + meta = await sharp(buf, { limitInputPixels: 80_000_000 }).metadata();
216 + } catch {
217 + markNegative(url, 'dead: not an image');
218 + return { ok: false, status: 'dead', reason: `not an image (${res.headers.get('content-type') ?? 'unknown type'})`, httpStatus: res.status };
219 + }
220 + const ext = EXT_BY_FORMAT[meta.format ?? ''] ?? 'jpg';
221 + const dest = origPath(key, ext);
222 + mkdirSync(path.dirname(dest), { recursive: true });
223 + const tmp = `${dest}.${process.pid}.tmp`;
224 + await writeFile(tmp, buf);
225 + await rename(tmp, dest);
226 + return { ok: true, info: { key, path: dest, ext, contentType: CT_BY_EXT[ext] ?? 'application/octet-stream', bytes: buf.byteLength, width: meta.width ?? null, height: meta.height ?? null, fromCache: false } };
227 + } catch (err) {
228 + const reason = err instanceof Error ? (err.name === 'AbortError' ? 'timeout' : err.message) : String(err);
229 + markNegative(url, `error: ${reason}`, 3600_000);
230 + return { ok: false, status: 'error', reason, httpStatus: null };
231 + } finally {
232 + clearTimeout(timer);
233 + release();
234 + }
235 +}
236 +
237 +const variantInflight = new Map<string, Promise<string>>();
238 +
239 +/** Produce (or reuse) a resized WebP/AVIF variant; returns its path. */
240 +export async function ensureVariant(info: OriginalInfo, width: ImageWidth, fmt: 'webp' | 'avif' = 'webp'): Promise<string> {
241 + const dest = variantPath(info.key, width, fmt);
242 + try {
243 + await stat(dest);
244 + return dest;
245 + } catch {
246 + /* build */
247 + }
248 + const k = `${dest}`;
249 + const existing = variantInflight.get(k);
250 + if (existing) return existing;
251 + const p = (async () => {
252 + mkdirSync(path.dirname(dest), { recursive: true });
253 + const tmp = `${dest}.${process.pid}.tmp`;
254 + let pipeline = sharp(info.path, { limitInputPixels: 80_000_000, animated: false }).rotate().resize({ width, withoutEnlargement: true, fit: 'inside' });
255 + pipeline = fmt === 'avif' ? pipeline.avif({ quality: 55, effort: 3 }) : pipeline.webp({ quality: 80, effort: 4 });
256 + await pipeline.toFile(tmp);
257 + await rename(tmp, dest);
258 + return dest;
259 + })().finally(() => variantInflight.delete(k));
260 + variantInflight.set(k, p);
261 + return p;
262 +}
263 +
264 +export async function readVariant(p: string): Promise<Buffer> {
265 + return readFile(p);
266 +}
267 +
268 +/** 64-bit difference hash (dHash) over a 9×8 grayscale thumbnail; robust to resize/recompression. */
269 +export function dhashFromGray(pixels: Uint8Array | number[], width = 9, height = 8): string {
270 + let bits = '';
271 + for (let y = 0; y < height; y++) {
272 + for (let x = 0; x < width - 1; x++) {
273 + const a = pixels[y * width + x]!;
274 + const b = pixels[y * width + x + 1]!;
275 + bits += a > b ? '1' : '0';
276 + }
277 + }
278 + return BigInt(`0b${bits}`).toString(16).padStart(16, '0');
279 +}
280 +
281 +export function hamming(a: string, b: string): number {
282 + const x = BigInt(`0x${a}`) ^ BigInt(`0x${b}`);
283 + let n = 0;
284 + let v = x;
285 + while (v) {
286 + n += Number(v & 1n);
287 + v >>= 1n;
288 + }
289 + return n;
290 +}
291 +
292 +export async function perceptualHash(filePath: string): Promise<string | null> {
293 + try {
294 + const { data } = await sharp(filePath, { limitInputPixels: 80_000_000 }).rotate().grayscale().resize(9, 8, { fit: 'fill' }).raw().toBuffer({ resolveWithObject: true });
295 + return dhashFromGray(data, 9, 8);
296 + } catch {
297 + return null;
298 + }
299 +}
added workers/image-processing/index.ts +182 −0
@@ -0,0 +1,182 @@
1 +/**
2 + * Image-processing worker (§113 basics): validates and caches product images, records
3 + * status/dimensions/perceptual hash in `images`, and promotes a replacement hero image when the
4 + * current one is dead. Shares the fetch/cache core with the web `/img` proxy.
5 + */
6 +import { and, eq, inArray, isNull, lt, or, sql } from 'drizzle-orm';
7 +import { assets, images } from '@rareindex/database';
8 +import { logger } from '@rareindex/shared';
9 +import sharp from 'sharp';
10 +import { db } from '../lib/db.ts';
11 +import { ensureOriginal, ensureVariant, imageKey, perceptualHash } from './core.ts';
12 +
13 +export interface ImagesResult {
14 + registered: number;
15 + checked: number;
16 + ok: number;
17 + dead: number;
18 + blocked: number;
19 + error: number;
20 + promoted: number;
21 + placeholders: number;
22 +}
23 +
24 +export interface ImagesOptions {
25 + limit?: number;
26 + recheck?: boolean;
27 + assetIds?: string[];
28 + concurrency?: number;
29 + /** also warm the 192/384 variants so first paint is instant */
30 + warm?: boolean;
31 +}
32 +
33 +const RECHECK_AFTER_DAYS = 30;
34 +const RETRY_ERROR_AFTER_HOURS = 6;
35 +
36 +export async function processImages(opts: ImagesOptions = {}): Promise<ImagesResult> {
37 + const log = logger.child({ component: 'images' });
38 + const limit = opts.limit ?? 2000;
39 + const res: ImagesResult = { registered: 0, checked: 0, ok: 0, dead: 0, blocked: 0, error: 0, promoted: 0, placeholders: 0 };
40 +
41 + // 1. Register hero URLs that have no images row yet (catalog sources may set hero_image_url directly).
42 + const assetFilter = opts.assetIds?.length ? sql` and a.id = any(${opts.assetIds}::text[])` : sql``;
43 + const reg = (await db().execute(sql`
44 + insert into images (id, asset_id, source_id, url, role, status)
45 + select 'img_' || substr(md5(a.id || a.hero_image_url), 1, 20), a.id, null, a.hero_image_url, 'hero', 'unchecked'
46 + from assets a
47 + where a.hero_image_url is not null and a.hero_image_url ~ '^https?://'
48 + and not exists (select 1 from images i where i.url = a.hero_image_url)${assetFilter}
49 + limit ${limit}
50 + on conflict (url) do nothing
51 + returning id
52 + `)) as unknown as unknown[];
53 + res.registered = reg.length;
54 +
55 + // 2. Pick rows to check.
56 + const staleCut = new Date(Date.now() - RECHECK_AFTER_DAYS * 86_400_000);
57 + const errorCut = new Date(Date.now() - RETRY_ERROR_AFTER_HOURS * 3600_000);
58 + const conds = [
59 + eq(images.status, 'unchecked'),
60 + and(eq(images.status, 'error'), lt(images.checkedAt, errorCut)),
61 + ...(opts.recheck ? [and(eq(images.status, 'ok'), or(isNull(images.checkedAt), lt(images.checkedAt, staleCut)))] : []),
62 + ];
63 + const rows = await db()
64 + .select({ id: images.id, url: images.url, assetId: images.assetId, role: images.role, width: images.width })
65 + .from(images)
66 + .where(and(or(...conds), opts.assetIds?.length ? inArray(images.assetId, opts.assetIds) : undefined))
67 + .orderBy(sql`case ${images.role} when 'hero' then 0 else 1 end`, images.createdAt)
68 + .limit(limit);
69 +
70 + const deadHeroAssets = new Set<string>();
71 + await pool(rows, opts.concurrency ?? 6, async (row) => {
72 + res.checked++;
73 + const outcome = await checkOne(row.url, opts.warm ?? true);
74 + if (outcome.status === 'ok') res.ok++;
75 + else if (outcome.status === 'dead') res.dead++;
76 + else if (outcome.status === 'blocked') res.blocked++;
77 + else res.error++;
78 + await db()
79 + .update(images)
80 + .set({ status: outcome.status, checkedAt: new Date(), cacheKey: imageKey(row.url), bytes: outcome.bytes, contentType: outcome.contentType, width: outcome.width ?? row.width ?? null, height: outcome.height ?? null, phash: outcome.phash ?? undefined, error: outcome.error })
81 + .where(eq(images.id, row.id));
82 + if (outcome.status !== 'ok' && outcome.status !== 'error' && row.assetId && row.role === 'hero') deadHeroAssets.add(row.assetId);
83 + });
84 +
85 + // 3. Promote a replacement hero for assets whose hero image is gone.
86 + for (const assetId of deadHeroAssets) {
87 + const promoted = await promoteHero(assetId);
88 + if (promoted) res.promoted++;
89 + else res.placeholders++;
90 + }
91 + log.info(res, 'images processed');
92 + return res;
93 +}
94 +
95 +interface CheckOutcome {
96 + status: 'ok' | 'dead' | 'blocked' | 'error';
97 + bytes: number | null;
98 + contentType: string | null;
99 + width: number | null;
100 + height: number | null;
101 + phash: string | null;
102 + error: string | null;
103 +}
104 +
105 +async function checkOne(url: string, warm: boolean): Promise<CheckOutcome> {
106 + const out = await ensureOriginal(url);
107 + if (!out.ok) return { status: out.status, bytes: null, contentType: null, width: null, height: null, phash: null, error: out.reason.slice(0, 200) };
108 + const info = out.info;
109 + let width = info.width;
110 + let height = info.height;
111 + if (width === null || height === null) {
112 + try {
113 + const meta = await sharp(info.path).metadata();
114 + width = meta.width ?? null;
115 + height = meta.height ?? null;
116 + } catch {
117 + /* keep nulls */
118 + }
119 + }
120 + const phash = await perceptualHash(info.path);
121 + if (warm) {
122 + try {
123 + await ensureVariant(info, 192);
124 + await ensureVariant(info, 384);
125 + } catch (err) {
126 + return { status: 'error', bytes: info.bytes, contentType: info.contentType, width, height, phash, error: `variant: ${err instanceof Error ? err.message : String(err)}`.slice(0, 200) };
127 + }
128 + }
129 + return { status: 'ok', bytes: info.bytes, contentType: info.contentType, width, height, phash, error: null };
130 +}
131 +
132 +/** Pick the best surviving image of an asset and make it the hero; returns false when none exists. */
133 +async function promoteHero(assetId: string): Promise<boolean> {
134 + const [asset] = await db().select({ hero: assets.heroImageUrl }).from(assets).where(eq(assets.id, assetId)).limit(1);
135 + if (!asset) return false;
136 + const candidates = await db()
137 + .select({ id: images.id, url: images.url, status: images.status })
138 + .from(images)
139 + .where(and(eq(images.assetId, assetId), inArray(images.status, ['ok', 'unchecked']), sql`${images.url} <> ${asset.hero ?? ''}`))
140 + .orderBy(sql`case ${images.status} when 'ok' then 0 else 1 end`, sql`case ${images.role} when 'hero' then 0 when 'gallery' then 1 else 2 end`, images.createdAt)
141 + .limit(6);
142 + for (const c of candidates) {
143 + let status = c.status;
144 + if (status === 'unchecked') {
145 + const outcome = await checkOne(c.url, true);
146 + status = outcome.status;
147 + await db()
148 + .update(images)
149 + .set({ status: outcome.status, checkedAt: new Date(), cacheKey: imageKey(c.url), bytes: outcome.bytes, contentType: outcome.contentType, width: outcome.width, height: outcome.height, phash: outcome.phash ?? undefined, error: outcome.error })
150 + .where(eq(images.id, c.id));
151 + }
152 + if (status === 'ok') {
153 + await db().update(assets).set({ heroImageUrl: c.url, updatedAt: new Date() }).where(eq(assets.id, assetId));
154 + await db().update(images).set({ role: 'hero' }).where(eq(images.id, c.id));
155 + return true;
156 + }
157 + }
158 + return false;
159 +}
160 +
161 +async function pool<T>(items: T[], concurrency: number, fn: (item: T) => Promise<void>): Promise<void> {
162 + let i = 0;
163 + const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
164 + while (i < items.length) {
165 + const item = items[i++]!;
166 + try {
167 + await fn(item);
168 + } catch (err) {
169 + logger.warn({ err }, 'image task failed');
170 + }
171 + }
172 + });
173 + await Promise.all(workers);
174 +}
175 +
176 +/** Summary for the admin/health views. */
177 +export async function imageStats(): Promise<Record<string, number>> {
178 + const rows = (await db().execute(sql`select status, count(*)::int as n from images group by status`)) as unknown as Array<{ status: string; n: number }>;
179 + const out: Record<string, number> = {};
180 + for (const r of rows) out[r.status] = r.n;
181 + return out;
182 +}
modified workers/lib/queue.ts +1 −0
@@ -20,6 +20,7 @@ export const JOBS = {
20 20 benchmarksSync: 'benchmarks.sync',
21 21 listingsExpire: 'listings.expire',
22 22 accountJobs: 'account.jobs',
23 + imagesProcess: 'images.process',
23 24 } as const;
24 25 export type JobName = (typeof JOBS)[keyof typeof JOBS];
25 26
modified workers/main.ts +7 −0
@@ -17,6 +17,7 @@ import { syncFx } from './fx.ts';
17 17 import { syncBenchmarks } from './benchmarks.ts';
18 18 import { computeHealth } from './health.ts';
19 19 import { expireListings } from './listings-expire.ts';
20 +import { processImages } from './image-processing/index.ts';
20 21
21 22 /**
22 23 * RareIndex worker process (PM2: rareindex-worker). One process runs the queue handlers and the
@@ -71,6 +72,7 @@ export async function startWorker(): Promise<() => Promise<void>> {
71 72 }
72 73 if ((await pendingCount()) > 0) for (let k = 0; k < RESOLVE_WORKERS; k++) await queue.send(JOBS.resolveBatch, {}, { singletonKey: `resolve:${k}`, singletonSeconds: 30, startAfterSeconds: 5 });
73 74 if (touched.size) await queue.send(JOBS.valuationAsset, { assetIds: [...touched].slice(0, 5000) }, { singletonKey: `value:${Date.now()}` });
75 + if (touched.size) await queue.send(JOBS.imagesProcess, { assetIds: [...touched].slice(0, 5000), limit: 5000 }, { singletonKey: `images:${Date.now()}` });
74 76 });
75 77 await queue.work<{ assetIds: string[] }>(JOBS.valuationAsset, { concurrency: 1, pollingIntervalSeconds: 5 }, async (data) => {
76 78 await valueMany(data.assetIds, { concurrency: 4 });
@@ -98,6 +100,9 @@ export async function startWorker(): Promise<() => Promise<void>> {
98 100 await queue.work(JOBS.benchmarksSync, { concurrency: 1, pollingIntervalSeconds: 60 }, async () => {
99 101 await syncBenchmarks();
100 102 });
103 + await queue.work<{ assetIds?: string[]; limit?: number; recheck?: boolean }>(JOBS.imagesProcess, { concurrency: 1, pollingIntervalSeconds: 30 }, async (data) => {
104 + await processImages({ assetIds: data.assetIds, limit: data.limit ?? 2000, recheck: data.recheck ?? false });
105 + });
101 106 await queue.work(JOBS.listingsExpire, { concurrency: 1, pollingIntervalSeconds: 60 }, async () => {
102 107 await expireListings();
103 108 });
@@ -120,6 +125,8 @@ export async function startWorker(): Promise<() => Promise<void>> {
120 125 await queue.schedule(JOBS.radarScan, '0 5 * * *', {});
121 126 await queue.schedule(JOBS.healthCompute, '*/30 * * * *', {});
122 127 await queue.schedule(JOBS.listingsExpire, '10 * * * *', {});
128 + await queue.schedule(JOBS.imagesProcess, '50 5 * * *', { limit: 20000, recheck: true });
129 + await queue.schedule(JOBS.imagesProcess, '*/15 * * * *', { limit: 3000 });
123 130 await queue.schedule(JOBS.accountJobs, '*/10 * * * *', {});
124 131
125 132 // ---- crawl scheduler loop ----
modified workers/package.json +1 −0
@@ -19,6 +19,7 @@
19 19 "drizzle-orm": "^0.45.0",
20 20 "pg-boss": "^12.0.0",
21 21 "postgres": "^3.4.7",
22 + "sharp": "^0.35.0",
22 23 "zod": "^4.0.0"
23 24 },
24 25 "devDependencies": {
25 26