From d78ac171257d7a6a811a1af3211b4180a017362d Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Thu, 7 May 2026 23:19:20 -0600 Subject: [PATCH 1/9] fix(etl): use raw SQL for completion write to bypass neon-http enum issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Drizzle ORM .set({ status: 'completed' }) with the neon-http driver appears to fail silently (triggering the catch block which then sets status='failed'), even though the identical pattern with 'failed' works. Switch to a raw sql`UPDATE ... SET status = 'completed'::etl_job_status` to match the pattern already used in updateEtlJobProgress, bypassing any Drizzle/neon-http enum serialization difference. Also isolate the completion write in its own try-catch so a transient failure here logs the error and leaves the job 'running' (for Reset Stuck) rather than cascading to 'failed'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../api/src/services/etl/processCatalogEtl.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/api/src/services/etl/processCatalogEtl.ts b/packages/api/src/services/etl/processCatalogEtl.ts index 01a8f77f2d..7c3ee865d4 100644 --- a/packages/api/src/services/etl/processCatalogEtl.ts +++ b/packages/api/src/services/etl/processCatalogEtl.ts @@ -160,10 +160,18 @@ export async function processCatalogETL({ const totalRows = rowIndex; - await db - .update(etlJobs) - .set({ status: 'completed', completedAt: new Date() }) - .where(eq(etlJobs.id, jobId)); + // Use raw SQL to avoid neon-http enum serialization issues with Drizzle ORM. + // Isolated try-catch so a transient DB hiccup here doesn't cascade to status='failed'. + try { + await db.execute( + sql`UPDATE etl_jobs SET status = 'completed'::etl_job_status, completed_at = NOW() WHERE id = ${jobId}`, + ); + } catch (completionErr) { + console.error( + `[ETL] Failed to mark job ${jobId} completed — will be reset by stuck-job sweep:`, + completionErr, + ); + } console.log(`🔍 [TRACE] ✅ Done processing ${objectKey} - ${totalRows} rows processed`); } catch (error) { From cb7ef0a4fca42e0145b8850f9273c92347d99623 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Thu, 7 May 2026 23:49:39 -0600 Subject: [PATCH 2/9] fix(etl): make catalog_items weight nullable + add ETL integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The weight NOT NULL constraint on catalog_items was causing ETL job failures for any item missing weight data (common for clothing/footwear brands). The CatalogItemValidator explicitly marks weight as optional, but the DB would reject the INSERT, causing processValidItemsBatch's fallback to also fail, which propagated to the outer catch and set status='failed'. Migration 0037 drops NOT NULL from weight and weight_unit on catalog_items. Adds full ETL integration test suite confirming: happy path completes, no-weight items don't fail, invalid-only runs still complete, exact/multi-batch row counts work, and the embedding fallback doesn't throw to the outer caller. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../drizzle/0037_nullable_catalog_weight.sql | 5 + packages/api/src/db/schema.ts | 4 +- packages/api/test/etl.test.ts | 243 ++++++++++++++++++ 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 packages/api/drizzle/0037_nullable_catalog_weight.sql create mode 100644 packages/api/test/etl.test.ts diff --git a/packages/api/drizzle/0037_nullable_catalog_weight.sql b/packages/api/drizzle/0037_nullable_catalog_weight.sql new file mode 100644 index 0000000000..8cec041368 --- /dev/null +++ b/packages/api/drizzle/0037_nullable_catalog_weight.sql @@ -0,0 +1,5 @@ +-- catalog_items.weight and weight_unit: drop NOT NULL to allow items without weight data. +-- The validator intentionally skips weight (clothing/footwear often omit it), but the +-- NOT NULL constraint was causing upserts to throw, which cascaded to ETL job failures. +ALTER TABLE "catalog_items" ALTER COLUMN "weight" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "catalog_items" ALTER COLUMN "weight_unit" DROP NOT NULL; diff --git a/packages/api/src/db/schema.ts b/packages/api/src/db/schema.ts index a14bf75aac..a16763444a 100644 --- a/packages/api/src/db/schema.ts +++ b/packages/api/src/db/schema.ts @@ -99,8 +99,8 @@ export const catalogItems = pgTable( name: text('name').notNull(), productUrl: text('product_url').notNull(), sku: text('sku').unique().notNull(), - weight: real('weight').notNull(), - weightUnit: text('weight_unit').notNull().$type(), + weight: real('weight'), + weightUnit: text('weight_unit').$type(), description: text('description'), categories: jsonb('categories').$type(), images: jsonb('images').$type(), diff --git a/packages/api/test/etl.test.ts b/packages/api/test/etl.test.ts new file mode 100644 index 0000000000..f70c70e917 --- /dev/null +++ b/packages/api/test/etl.test.ts @@ -0,0 +1,243 @@ +import { createDbClient } from '@packrat/api/db'; +import { catalogItems, etlJobs, invalidItemLogs } from '@packrat/api/db/schema'; +import { processCatalogETL } from '@packrat/api/services/etl/processCatalogEtl'; +import { processValidItemsBatch } from '@packrat/api/services/etl/processValidItemsBatch'; +import { R2BucketService } from '@packrat/api/services/r2-bucket'; +import { count, eq } from 'drizzle-orm'; +import { describe, expect, it, vi } from 'vitest'; + +// ── CSV helpers ─────────────────────────────────────────────────────────────── + +const CSV_HEADER = 'name,sku,productUrl,brand,price,weight,weightUnit\n'; +const CSV_ROW = (i: number) => + `Test Item ${i},SKU-${i},https://example.com/item-${i},TestBrand,49.99,500,g\n`; + +function makeCsv(rows: number): string { + return CSV_HEADER + Array.from({ length: rows }, (_, i) => CSV_ROW(i)).join(''); +} + +function makeReadableStream(text: string): ReadableStream { + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + return new ReadableStream({ + start(controller) { + controller.enqueue(bytes); + controller.close(); + }, + }); +} + +// ── Mock R2BucketService to return a CSV stream ─────────────────────────────── + +function mockR2WithCsv(csv: string) { + vi.mocked(R2BucketService).mockImplementationOnce( + () => + ({ + get: vi.fn().mockResolvedValue({ body: makeReadableStream(csv) }), + }) as any, + ); +} + +function mockR2WithNull() { + vi.mocked(R2BucketService).mockImplementationOnce( + () => + ({ + get: vi.fn().mockResolvedValue(null), + }) as any, + ); +} + +// ── DB helpers ──────────────────────────────────────────────────────────────── + +async function insertJob(jobId: string) { + const db = createDbClient({} as any); + await db.insert(etlJobs).values({ + id: jobId, + status: 'running', + source: 'test', + filename: 'test.csv', + scraperRevision: 'abc123', + startedAt: new Date(), + }); +} + +async function getJob(jobId: string) { + const db = createDbClient({} as any); + const rows = await db.select().from(etlJobs).where(eq(etlJobs.id, jobId)); + return rows[0]; +} + +// minimal env — createDbClient and R2BucketService are both globally mocked +const TEST_ENV = { + NEON_DATABASE_URL: 'postgres://test_user:test_password@localhost:5432/packrat_test', + OPENAI_API_KEY: 'sk-test', + AI_PROVIDER: 'openai', + CLOUDFLARE_ACCOUNT_ID: 'test-account-id', + CLOUDFLARE_AI_GATEWAY_ID: 'test-gateway-id', +} as unknown as Parameters[0]['env']; + +function makeMessage(jobId: string) { + return { id: jobId, data: { objectKey: 'v2/test/test.csv' } }; +} + +// ───────────────────────────────────────────────────────────────────────────── + +describe('processCatalogETL', () => { + it('marks job as completed after processing a valid CSV', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + mockR2WithCsv(makeCsv(5)); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const job = await getJob(jobId); + expect(job?.status, 'job should be completed, not failed').toBe('completed'); + expect(job?.completedAt).not.toBeNull(); + expect(job?.totalProcessed).toBe(5); + }); + + it('writes catalog items to the DB', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + mockR2WithCsv(makeCsv(3)); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const db = createDbClient({} as any); + const [result] = await db.select({ total: count() }).from(catalogItems); + expect(result?.total).toBeGreaterThanOrEqual(3); + }); + + it('marks job as completed when all rows are invalid (writes invalid logs)', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + // Missing sku and productUrl → all rows invalid + mockR2WithCsv('name,brand\nItem Without SKU,TestBrand\n'); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const job = await getJob(jobId); + expect(job?.status, 'job with only invalid rows should still complete').toBe('completed'); + + const db = createDbClient({} as any); + const [logResult] = await db + .select({ total: count() }) + .from(invalidItemLogs) + .where(eq(invalidItemLogs.jobId, jobId)); + expect(logResult?.total).toBeGreaterThan(0); + }); + + it('marks job as failed and rethrows when R2 object is missing', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + mockR2WithNull(); + + await expect( + processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }), + ).rejects.toThrow(); + + const job = await getJob(jobId); + expect(job?.status).toBe('failed'); + }); + + it('handles exactly BATCH_SIZE rows (no remainder flush — edge case)', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + mockR2WithCsv(makeCsv(100)); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const job = await getJob(jobId); + expect(job?.status, 'exact BATCH_SIZE rows should complete').toBe('completed'); + expect(job?.totalProcessed).toBe(100); + }); + + it('handles rows spanning multiple batches', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + mockR2WithCsv(makeCsv(250)); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const job = await getJob(jobId); + expect(job?.status).toBe('completed'); + expect(job?.totalProcessed).toBe(250); + }); + + it('marks job as completed even when items have no weight', async () => { + const jobId = crypto.randomUUID(); + await insertJob(jobId); + // No weight column — valid items that previously caused NOT NULL DB failures + mockR2WithCsv( + 'name,sku,productUrl,brand\nNo Weight Item,SKU-NW,https://example.com/nw,TestBrand\n', + ); + + await processCatalogETL({ message: makeMessage(jobId) as any, env: TEST_ENV }); + + const job = await getJob(jobId); + expect(job?.status, 'items without weight should not cause job failure').toBe('completed'); + }); +}); + +describe('processValidItemsBatch', () => { + it('does not throw when embedding generation fails (falls back gracefully)', async () => { + const jobId = crypto.randomUUID(); + const db = createDbClient({} as any); + await db.insert(etlJobs).values({ + id: jobId, + status: 'running', + source: 'test', + filename: 'test.csv', + scraperRevision: 'abc123', + startedAt: new Date(), + }); + + const { generateManyEmbeddings } = await import('@packrat/api/services/embeddingService'); + vi.mocked(generateManyEmbeddings).mockRejectedValueOnce(new Error('OpenAI rate limit')); + + await expect( + processValidItemsBatch({ + jobId, + items: [ + { + name: 'Test Item', + sku: 'SKU-EMBED-001', + productUrl: 'https://example.com/item', + brand: 'TestBrand', + weight: 500, + weightUnit: 'g', + } as any, + ], + env: TEST_ENV, + }), + ).resolves.not.toThrow(); + }); + + it('does not throw when items have no weight', async () => { + const jobId = crypto.randomUUID(); + const db = createDbClient({} as any); + await db.insert(etlJobs).values({ + id: jobId, + status: 'running', + source: 'test', + filename: 'test.csv', + scraperRevision: 'abc123', + startedAt: new Date(), + }); + + await expect( + processValidItemsBatch({ + jobId, + items: [ + { + name: 'No Weight Item', + sku: 'SKU-NW-002', + productUrl: 'https://example.com/nw', + brand: 'TestBrand', + } as any, + ], + env: TEST_ENV, + }), + ).resolves.not.toThrow(); + }); +}); From 60d5be8be2d66627ff177812233a5248f3089348 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Fri, 8 May 2026 19:50:36 -0600 Subject: [PATCH 3/9] fix(db): regenerate migration with drizzle-kit (0037_rich_electro) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the handwritten 0037_nullable_catalog_weight.sql with the drizzle-kit generated equivalent — same ALTER TABLE statements but now tracked in the drizzle journal with the proper snapshot. Social feed table CREATE statements were stripped from the generated output because 0033_social_feed_tables.sql was applied manually and not tracked in the journal, causing drizzle-kit to emit duplicate DDL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/api/drizzle/0037_rich_electro.sql | 2 + packages/api/drizzle/meta/0037_snapshot.json | 2070 ++++++++++++++++++ packages/api/drizzle/meta/_journal.json | 7 + 3 files changed, 2079 insertions(+) create mode 100644 packages/api/drizzle/0037_rich_electro.sql create mode 100644 packages/api/drizzle/meta/0037_snapshot.json diff --git a/packages/api/drizzle/0037_rich_electro.sql b/packages/api/drizzle/0037_rich_electro.sql new file mode 100644 index 0000000000..de1571ce17 --- /dev/null +++ b/packages/api/drizzle/0037_rich_electro.sql @@ -0,0 +1,2 @@ +ALTER TABLE "catalog_items" ALTER COLUMN "weight" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "catalog_items" ALTER COLUMN "weight_unit" DROP NOT NULL; diff --git a/packages/api/drizzle/meta/0037_snapshot.json b/packages/api/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000000..cb2590a262 --- /dev/null +++ b/packages/api/drizzle/meta/0037_snapshot.json @@ -0,0 +1,2070 @@ +{ + "id": "a4b61d9f-bdf1-486a-8331-bcde8ae3c8a0", + "prevId": "fa3d18d1-67a7-488a-aba5-5b18295e80f2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_providers": { + "name": "auth_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_providers_user_id_users_id_fk": { + "name": "auth_providers_user_id_users_id_fk", + "tableFrom": "auth_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_likes": { + "name": "comment_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_likes_comment_id_post_comments_id_fk": { + "name": "comment_likes_comment_id_post_comments_id_fk", + "tableFrom": "comment_likes", + "tableTo": "post_comments", + "columnsFrom": ["comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_likes_user_id_users_id_fk": { + "name": "comment_likes_user_id_users_id_fk", + "tableFrom": "comment_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comment_likes_comment_id_user_id_unique": { + "name": "comment_likes_comment_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["comment_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.one_time_passwords": { + "name": "one_time_passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_passwords_user_id_users_id_fk": { + "name": "one_time_passwords_user_id_users_id_fk", + "tableFrom": "one_time_passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_comments": { + "name": "post_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_comments_post_id_posts_id_fk": { + "name": "post_comments_post_id_posts_id_fk", + "tableFrom": "post_comments", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_user_id_users_id_fk": { + "name": "post_comments_user_id_users_id_fk", + "tableFrom": "post_comments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_parent_comment_id_post_comments_id_fk": { + "name": "post_comments_parent_comment_id_post_comments_id_fk", + "tableFrom": "post_comments", + "tableTo": "post_comments", + "columnsFrom": ["parent_comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_likes": { + "name": "post_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_likes_post_id_posts_id_fk": { + "name": "post_likes_post_id_posts_id_fk", + "tableFrom": "post_likes", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_likes_user_id_users_id_fk": { + "name": "post_likes_user_id_users_id_fk", + "tableFrom": "post_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_likes_post_id_user_id_unique": { + "name": "post_likes_post_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["post_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token": { + "name": "replaced_by_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index 70f2f73413..b514164bd9 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -267,6 +267,13 @@ "when": 1775883868581, "tag": "0036_typical_zuras", "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1778291376040, + "tag": "0037_rich_electro", + "breakpoints": true } ] } From f694d960fadcb2c4a9d07d088b4cf647c61d8a7c Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 08:00:42 -0600 Subject: [PATCH 4/9] fix(etl): handle nullable weight/weightUnit when building pack items from catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Making catalog_items.weight/weight_unit nullable caused a TypeScript error in packService.ts — pack items require non-null weight. Fall back to 0/'g' so the AI-generated pack flow still compiles; user can edit after generation. --- packages/api/src/services/packService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/services/packService.ts b/packages/api/src/services/packService.ts index 44ff2ed716..8aa7bff679 100644 --- a/packages/api/src/services/packService.ts +++ b/packages/api/src/services/packService.ts @@ -150,8 +150,8 @@ export class PackService { catalogItemId: catalogItem.id, name: catalogItem.name, description: catalogItem.description, - weight: catalogItem.weight, - weightUnit: catalogItem.weightUnit, + weight: catalogItem.weight ?? 0, + weightUnit: catalogItem.weightUnit ?? 'g', image: catalogItem.images?.[0], }; }) From 6f84a52e205981552dc1a656f671b2ab319ee07f Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 08:07:43 -0600 Subject: [PATCH 5/9] fix(admin,etl): fix CORS Access-Control-Allow-Origin missing on preflight + regenerate weight migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CORS: admin scoped cors was silently dropping Access-Control-Allow-Origin on preflight (two stacked cors plugins conflicted — root sets credentials:false/*, admin sets credentials:true/specific-origin, header got dropped). Switch to origin function so Elysia reflects the exact origin back; bypass auth guard for OPTIONS preflights. Fixes admin app CORS errors from https://admin.packratai.com. Migration: regenerate weight/weight_unit nullable migration as 0047 — main merged 0037–0046 after this branch was cut. --- .../api/drizzle/0047_cute_bloodscream.sql | 2 + .../drizzle/meta/0047_cute_bloodscream.json | 2257 +++++++++++++++++ packages/api/drizzle/meta/_journal.json | 7 + packages/api/src/routes/admin/index.ts | 12 +- 4 files changed, 2277 insertions(+), 1 deletion(-) create mode 100644 packages/api/drizzle/0047_cute_bloodscream.sql create mode 100644 packages/api/drizzle/meta/0047_cute_bloodscream.json diff --git a/packages/api/drizzle/0047_cute_bloodscream.sql b/packages/api/drizzle/0047_cute_bloodscream.sql new file mode 100644 index 0000000000..18c813ecf1 --- /dev/null +++ b/packages/api/drizzle/0047_cute_bloodscream.sql @@ -0,0 +1,2 @@ +ALTER TABLE "catalog_items" ALTER COLUMN "weight" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "catalog_items" ALTER COLUMN "weight_unit" DROP NOT NULL; \ No newline at end of file diff --git a/packages/api/drizzle/meta/0047_cute_bloodscream.json b/packages/api/drizzle/meta/0047_cute_bloodscream.json new file mode 100644 index 0000000000..cce3b7f09e --- /dev/null +++ b/packages/api/drizzle/meta/0047_cute_bloodscream.json @@ -0,0 +1,2257 @@ +{ + "id": "1f086d6d-055d-4b37-a5d6-32b1141d2043", + "prevId": "548299d6-dc62-4a37-893b-932e6b7451a1", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_users_id_fk": { + "name": "account_user_id_users_id_fk", + "tableFrom": "account", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "account_provider_account_idx": { + "name": "account_provider_account_idx", + "nullsNotDistinct": false, + "columns": ["provider_id", "account_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comment_likes": { + "name": "comment_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comment_likes_comment_id_post_comments_id_fk": { + "name": "comment_likes_comment_id_post_comments_id_fk", + "tableFrom": "comment_likes", + "tableTo": "post_comments", + "columnsFrom": ["comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comment_likes_user_id_users_id_fk": { + "name": "comment_likes_user_id_users_id_fk", + "tableFrom": "comment_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "comment_likes_comment_id_user_id_unique": { + "name": "comment_likes_comment_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["comment_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_comments": { + "name": "post_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_comments_post_id_posts_id_fk": { + "name": "post_comments_post_id_posts_id_fk", + "tableFrom": "post_comments", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_user_id_users_id_fk": { + "name": "post_comments_user_id_users_id_fk", + "tableFrom": "post_comments", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_comments_parent_comment_id_post_comments_id_fk": { + "name": "post_comments_parent_comment_id_post_comments_id_fk", + "tableFrom": "post_comments", + "tableTo": "post_comments", + "columnsFrom": ["parent_comment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_likes": { + "name": "post_likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "post_likes_post_id_posts_id_fk": { + "name": "post_likes_post_id_posts_id_fk", + "tableFrom": "post_likes", + "tableTo": "posts", + "columnsFrom": ["post_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_likes_user_id_users_id_fk": { + "name": "post_likes_user_id_users_id_fk", + "tableFrom": "post_likes", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "post_likes_post_id_user_id_unique": { + "name": "post_likes_post_id_user_id_unique", + "nullsNotDistinct": false, + "columns": ["post_id", "user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "posts_user_id_users_id_fk": { + "name": "posts_user_id_users_id_fk", + "tableFrom": "posts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_users_id_fk": { + "name": "session_user_id_users_id_fk", + "tableFrom": "session", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trail_condition_reports": { + "name": "trail_condition_reports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trail_name": { + "name": "trail_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trail_region": { + "name": "trail_region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "overall_condition": { + "name": "overall_condition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hazards": { + "name": "hazards", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "water_crossings": { + "name": "water_crossings", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "water_crossing_difficulty": { + "name": "water_crossing_difficulty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photos": { + "name": "photos", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trip_id": { + "name": "trip_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "trail_condition_reports_user_id_idx": { + "name": "trail_condition_reports_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_active_created_idx": { + "name": "trail_condition_reports_active_created_idx", + "columns": [ + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trail_name_idx": { + "name": "trail_condition_reports_trail_name_idx", + "columns": [ + { + "expression": "trail_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "trail_condition_reports_trip_id_idx": { + "name": "trail_condition_reports_trip_id_idx", + "columns": [ + { + "expression": "trip_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"trail_condition_reports\".\"trip_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trail_condition_reports_user_id_users_id_fk": { + "name": "trail_condition_reports_user_id_users_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "trail_condition_reports_trip_id_trips_id_fk": { + "name": "trail_condition_reports_trip_id_trips_id_fk", + "tableFrom": "trail_condition_reports", + "tableTo": "trips", + "columnsFrom": ["trip_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trail_osm_id": { + "name": "trail_osm_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USER'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index af35b27774..ca463a5058 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -330,6 +330,13 @@ "when": 1777803600000, "tag": "0046_social_feed_tables_uuid", "breakpoints": true + }, + { + "idx": 46, + "version": "7", + "when": 1778594728740, + "tag": "0047_cute_bloodscream", + "breakpoints": true } ] } diff --git a/packages/api/src/routes/admin/index.ts b/packages/api/src/routes/admin/index.ts index d40d9db318..0ab63aeac9 100644 --- a/packages/api/src/routes/admin/index.ts +++ b/packages/api/src/routes/admin/index.ts @@ -117,7 +117,16 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) // is rejected by browsers per the CORS spec). .use( cors({ - origin: 'https://admin.packratai.com', + // With credentials:true the browser requires a specific origin (not *). + // Reflect origin back when it's in our allowlist. + origin: (request) => { + const origin = request.headers.get('origin'); + if (!origin) return false; + if (origin === 'https://admin.packratai.com') return true; + if (origin.endsWith('.workers.dev')) return true; + if (/^https?:\/\/localhost(:\d+)?$/.test(origin)) return true; + return false; + }, credentials: true, allowedHeaders: ['Authorization', 'Content-Type'], }), @@ -172,6 +181,7 @@ export const adminRoutes = new Elysia({ prefix: '/admin' }) ) .onBeforeHandle(async ({ request, path }) => { if (path === '/api/admin/token') return; + if (request.method === 'OPTIONS') return; const ok = await adminAuthGuard(request); if (!ok) return status(401, { error: 'Unauthorized' }); }) From a28c16c42a12f9f34f1e824b6739b8e3059cb481 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 08:57:06 -0600 Subject: [PATCH 6/9] fix(types): resolve TypeScript errors blocking CI checks job - admin api.ts: double-cast Eden Treaty responses to PaginatedResponse (treaty infers wide union types that don't overlap with the interface) - packages/app user queries: remove deleted auth hooks (useLoginMutation, useRegisterMutation) that called Better Auth routes not in Elysia; redirect useCurrentUser to client.user.profile.get() - apps/web auth page: implement login/register locally via Better Auth REST endpoints instead of importing from @packrat/app - apps/trails useAuth: replace (apiClient as any).auth.* calls with typed trailsAuthClient (createAuthClient from better-auth/react); add auth-client.ts - apps/trails UserInfoSchema: id is UUID string not number (Better Auth) - apps/web types: Post.userId and PostAuthor.id are UUID strings not numbers - apps/web data.ts: update mock post IDs to match string type Co-Authored-By: Claude --- apps/admin/lib/api.ts | 12 ++- apps/admin/lib/queryKeys.ts | 7 ++ apps/trails/components/AuthGate.tsx | 11 ++- apps/trails/lib/auth-client.ts | 8 ++ apps/trails/lib/auth.ts | 2 +- apps/trails/lib/useAuth.tsx | 89 ++++++++++++----------- apps/web/app/auth/page.tsx | 47 ++++++++++-- apps/web/lib/data.ts | 20 ++--- apps/web/lib/types.ts | 4 +- packages/app/src/entities/user/index.ts | 2 - packages/app/src/entities/user/queries.ts | 29 +------- 11 files changed, 135 insertions(+), 96 deletions(-) create mode 100644 apps/trails/lib/auth-client.ts diff --git a/apps/admin/lib/api.ts b/apps/admin/lib/api.ts index e86364752f..19fd3ce416 100644 --- a/apps/admin/lib/api.ts +++ b/apps/admin/lib/api.ts @@ -103,7 +103,7 @@ export async function getUsers({ query: { limit, offset, q, includeDeleted: includeDeleted ? 'true' : undefined }, }); if (error) throwOnError(error); - return unwrap(data, 'users'); + return unwrap(data, 'users') as unknown as PaginatedResponse; } export async function deleteUser(id: number): Promise<{ success: boolean }> { @@ -158,7 +158,7 @@ export async function getPacks({ query: { limit, offset, q, includeDeleted: includeDeleted ? 'true' : undefined }, }); if (error) throwOnError(error); - return unwrap(data, 'packs'); + return unwrap(data, 'packs') as unknown as PaginatedResponse; } export async function deletePack(id: string): Promise<{ success: boolean }> { @@ -219,7 +219,7 @@ export async function getCatalogItems({ query: { limit, offset, q }, }); if (error) throwOnError(error); - return unwrap(data, 'catalog'); + return unwrap(data, 'catalog') as unknown as PaginatedResponse; } export async function deleteCatalogItem(id: number): Promise<{ success: boolean }> { @@ -377,6 +377,12 @@ export async function deleteTrailCondition(reportId: string): Promise<{ success: return unwrap(data, 'deleteTrailCondition'); } +async function adminFetch(path: string, init?: RequestInit): Promise { + const res = await adminFetcher(`${API_BASE}/api/admin${path}`, init); + if (!res.ok) throw new Error(`Admin API error: ${res.status}`); + return res.json() as Promise; +} + export function resetStuckEtlJobs(): Promise<{ reset: number; ids: string[] }> { return adminFetch('/analytics/catalog/etl/reset-stuck', { method: 'POST' }); } diff --git a/apps/admin/lib/queryKeys.ts b/apps/admin/lib/queryKeys.ts index 54d83d9626..ef1038b37d 100644 --- a/apps/admin/lib/queryKeys.ts +++ b/apps/admin/lib/queryKeys.ts @@ -37,6 +37,13 @@ export const queryKeys = { breakdown: () => [...queryKeys.platform.all(), 'breakdown'] as const, }, + osm: { + all: () => ['osm'] as const, + search: (q?: string, sport?: string) => [...queryKeys.osm.all(), 'search', q, sport] as const, + trail: (osmId: string) => [...queryKeys.osm.all(), 'trail', osmId] as const, + conditions: (q?: string) => [...queryKeys.osm.all(), 'conditions', q] as const, + }, + catalogAnalytics: { all: () => ['catalogAnalytics'] as const, overview: () => [...queryKeys.catalogAnalytics.all(), 'overview'] as const, diff --git a/apps/trails/components/AuthGate.tsx b/apps/trails/components/AuthGate.tsx index 0967e46c7f..be1718de6d 100644 --- a/apps/trails/components/AuthGate.tsx +++ b/apps/trails/components/AuthGate.tsx @@ -16,7 +16,7 @@ import { Loader2 } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; import { VerifyEmail } from 'trails-app/components/VerifyEmail'; -import { apiClient } from 'trails-app/lib/apiClient'; +import { trailsAuthClient } from 'trails-app/lib/auth-client'; import { useAuth } from 'trails-app/lib/useAuth'; const TABS = ['register', 'login', 'forgot'] as const; @@ -77,7 +77,14 @@ export function AuthGate() { e.preventDefault(); setLoading(true); try { - await apiClient.auth['forgot-password'].post({ email: forgotEmail }); + const { error } = await trailsAuthClient.requestPasswordReset({ + email: forgotEmail, + redirectTo: + typeof window !== 'undefined' + ? `${window.location.origin}/reset-password` + : '/reset-password', + }); + if (error) throw new Error(error.message); setForgotSent(true); } catch { toast.error('Could not send reset email. Try again.'); diff --git a/apps/trails/lib/auth-client.ts b/apps/trails/lib/auth-client.ts new file mode 100644 index 0000000000..a12b75ffae --- /dev/null +++ b/apps/trails/lib/auth-client.ts @@ -0,0 +1,8 @@ +'use client'; + +import { createAuthClient } from 'better-auth/react'; +import { trailsEnv } from 'trails-app/lib/env'; + +export const trailsAuthClient = createAuthClient({ + baseURL: trailsEnv.NEXT_PUBLIC_API_URL, +}); diff --git a/apps/trails/lib/auth.ts b/apps/trails/lib/auth.ts index 11198e198a..88cc129beb 100644 --- a/apps/trails/lib/auth.ts +++ b/apps/trails/lib/auth.ts @@ -39,7 +39,7 @@ export function clearTokens(): void { } export const UserInfoSchema = z.object({ - id: z.number(), + id: z.string(), email: z.string(), firstName: z.string().nullish(), lastName: z.string().nullish(), diff --git a/apps/trails/lib/useAuth.tsx b/apps/trails/lib/useAuth.tsx index 3b7bf4abef..ef4c8b473f 100644 --- a/apps/trails/lib/useAuth.tsx +++ b/apps/trails/lib/useAuth.tsx @@ -1,30 +1,28 @@ 'use client'; -import { asStringRecord, fromZod } from '@packrat/guards'; +import { fromZod } from '@packrat/guards'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { apiClient } from 'trails-app/lib/apiClient'; import { clearTokens, clearUser, getAccessToken, - getRefreshToken, getUser, setTokens, setUser, type UserInfo, UserInfoSchema, } from 'trails-app/lib/auth'; +import { trailsAuthClient } from 'trails-app/lib/auth-client'; interface AuthState { isAuthed: boolean; user: UserInfo | null; - // Pending verification: user registered but hasn't verified email yet pendingEmail: string | null; } interface AuthActions { register(email: string, opts: { password: string; firstName?: string }): Promise; - verifyEmail(otp: string): Promise; + verifyEmail(token: string): Promise; resendVerification(): Promise; login(email: string, password: string): Promise; logout(): Promise; @@ -35,10 +33,19 @@ interface AuthActions { const AuthContext = createContext<(AuthState & AuthActions) | null>(null); -function apiError(error: unknown, fallback: string): Error { - const rec = asStringRecord(error); - const msg = rec.error ?? rec.message; - return new Error(msg ?? fallback); +function parseAuthUser(user: { + id: string; + email: string; + [key: string]: unknown; +}): UserInfo | null { + return ( + fromZod(UserInfoSchema)({ + id: user.id, + email: user.email, + firstName: (user.firstName as string | null | undefined) ?? null, + lastName: (user.lastName as string | null | undefined) ?? null, + }) ?? null + ); } export function AuthProvider({ children }: { children: React.ReactNode }) { @@ -60,32 +67,40 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const register = useCallback( async (email: string, opts: { password: string; firstName?: string }) => { - const { error, status } = await apiClient.auth.register.post({ + const name = opts.firstName ?? email; + const { data, error } = await trailsAuthClient.signUp.email({ email, password: opts.password, - firstName: opts.firstName, + name, }); - if (error) throw apiError(error.value, `Registration failed: ${status}`); - setState((s) => ({ ...s, pendingEmail: email })); + if (error) throw new Error(error.message ?? 'Registration failed'); + if (data?.token) { + // autoSignIn: true succeeded — token is the Bearer session token + const parsedUser = parseAuthUser(data.user as Parameters[0]); + if (!parsedUser) throw new Error('Registration failed: unexpected user shape'); + setTokens(data.token, ''); + setUser(parsedUser); + setState({ isAuthed: true, user: parsedUser, pendingEmail: null }); + setAuthGateOpen(false); + } else { + setState((s) => ({ ...s, pendingEmail: email })); + } }, [], ); const verifyEmail = useCallback( - async (otp: string) => { + async (token: string) => { if (!state.pendingEmail) throw new Error('No pending email verification'); - const { data, error, status } = await apiClient.auth['verify-email'].post({ - email: state.pendingEmail, - code: otp, - }); - if (error || !data) throw apiError(error?.value, `Verification failed: ${status}`); - const { accessToken, refreshToken, user } = data; - if (!accessToken || !refreshToken || !user) { - throw new Error('Verification failed: missing token data'); + const { error } = await trailsAuthClient.verifyEmail({ query: { token } }); + if (error) throw new Error(error.message ?? 'Verification failed'); + const sessionRes = await trailsAuthClient.getSession(); + if (!sessionRes.data?.session || !sessionRes.data.user) { + throw new Error('Verification failed: could not get session'); } - const parsedUser = fromZod(UserInfoSchema)(user); + const parsedUser = parseAuthUser(sessionRes.data.user as Parameters[0]); if (!parsedUser) throw new Error('Verification failed: unexpected user shape'); - setTokens(accessToken, refreshToken); + setTokens(sessionRes.data.session.token, ''); setUser(parsedUser); setState({ isAuthed: true, user: parsedUser, pendingEmail: null }); setAuthGateOpen(false); @@ -95,36 +110,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const resendVerification = useCallback(async () => { if (!state.pendingEmail) throw new Error('No pending email'); - const { error, status } = await apiClient.auth['resend-verification'].post({ + const { error } = await trailsAuthClient.sendVerificationEmail({ email: state.pendingEmail, + callbackURL: typeof window !== 'undefined' ? window.location.origin : '', }); - if (error) throw apiError(error.value, `Resend failed: ${status}`); + if (error) throw new Error(error.message ?? 'Resend failed'); }, [state.pendingEmail]); const login = useCallback(async (email: string, password: string) => { - const { data, error, status } = await apiClient.auth.login.post({ email, password }); - if (error || !data) throw apiError(error?.value, `Login failed: ${status}`); - const { accessToken, refreshToken, user } = data; - if (!accessToken || !refreshToken || !user) { - throw new Error('Login failed: missing token data'); - } - const parsedUser = fromZod(UserInfoSchema)(user); + const { data, error } = await trailsAuthClient.signIn.email({ email, password }); + if (error || !data) throw new Error(error?.message ?? 'Login failed'); + const parsedUser = parseAuthUser(data.user as Parameters[0]); if (!parsedUser) throw new Error('Login failed: unexpected user shape'); - setTokens(accessToken, refreshToken); + setTokens(data.token, ''); setUser(parsedUser); setState({ isAuthed: true, user: parsedUser, pendingEmail: null }); setAuthGateOpen(false); }, []); const logout = useCallback(async () => { - const refreshToken = getRefreshToken(); - if (refreshToken) { - try { - await apiClient.auth.logout.post({ refreshToken }); - } catch { - // ignore — clear tokens regardless - } - } + await trailsAuthClient.signOut(); clearTokens(); clearUser(); setState({ isAuthed: false, user: null, pendingEmail: null }); diff --git a/apps/web/app/auth/page.tsx b/apps/web/app/auth/page.tsx index 400794c569..737937334b 100644 --- a/apps/web/app/auth/page.tsx +++ b/apps/web/app/auth/page.tsx @@ -1,10 +1,47 @@ 'use client'; -import { useLoginMutation, useRegisterMutation } from '@packrat/app'; +import { webEnv } from '@packrat/env/web'; +import { useMutation } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import type React from 'react'; import { useState } from 'react'; import { setTokens } from 'web-app/lib/auth'; +const API_BASE = webEnv.NEXT_PUBLIC_API_URL ?? 'http://localhost:8787'; + +function useLoginMutation() { + return useMutation({ + mutationFn: async (body: { email: string; password: string }) => { + const res = await fetch(`${API_BASE}/api/auth/sign-in/email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error('Login failed'); + return res.json() as Promise<{ token?: string; user?: unknown }>; + }, + }); +} + +function useRegisterMutation() { + return useMutation({ + mutationFn: async (body: { + email: string; + password: string; + firstName?: string; + lastName?: string; + }) => { + const name = [body.firstName, body.lastName].filter(Boolean).join(' ') || body.email; + const res = await fetch(`${API_BASE}/api/auth/sign-up/email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: body.email, password: body.password, name }), + }); + if (!res.ok) throw new Error('Registration failed'); + return res.json() as Promise; + }, + }); +} + export default function AuthPage() { const [tab, setTab] = useState<'login' | 'register'>('login'); const [email, setEmail] = useState(''); @@ -23,11 +60,9 @@ export default function AuthPage() { { email, password }, { onSuccess: (data) => { - const response = data as { accessToken?: string; refreshToken?: string } | null; - const accessToken = response?.accessToken ?? ''; - const refreshToken = response?.refreshToken ?? ''; - if (!accessToken) return; - setTokens(accessToken, refreshToken); + const token = (data as { token?: string }).token ?? ''; + if (!token) return; + setTokens(token, ''); router.push('/'); }, }, diff --git a/apps/web/lib/data.ts b/apps/web/lib/data.ts index a6f66b6df7..9948e024bf 100644 --- a/apps/web/lib/data.ts +++ b/apps/web/lib/data.ts @@ -1031,65 +1031,65 @@ export const mockCatalogItems: CatalogItem[] = [ export const mockPosts: Post[] = [ { id: 1, - userId: 2, + userId: '2', caption: 'Finally dialed in my SoCal desert PCT setup. 6.5lb base weight! The key was switching to a tarp and going stoveless for the desert section.', images: [], createdAt: '2024-07-28T14:30:00Z', updatedAt: '2024-07-28T14:30:00Z', - author: { id: 2, firstName: 'Sarah', lastName: 'Chen' }, + author: { id: '2', firstName: 'Sarah', lastName: 'Chen' }, likeCount: 142, commentCount: 23, likedByMe: false, }, { id: 2, - userId: 3, + userId: '3', caption: 'Wind River High Route gear list. This is what 4000+ miles of thru-hiking has taught me. Every gram has been earned.', images: [], createdAt: '2024-07-25T09:15:00Z', updatedAt: '2024-07-25T09:15:00Z', - author: { id: 3, firstName: 'Jake', lastName: 'Morrison' }, + author: { id: '3', firstName: 'Jake', lastName: 'Morrison' }, likeCount: 89, commentCount: 11, likedByMe: true, }, { id: 3, - userId: 4, + userId: '4', caption: 'AT weekend basecamp setup. Not the lightest but extremely comfortable for section hiking with my kids.', images: [], createdAt: '2024-07-22T16:45:00Z', updatedAt: '2024-07-22T16:45:00Z', - author: { id: 4, firstName: 'Maria', lastName: 'Santos' }, + author: { id: '4', firstName: 'Maria', lastName: 'Santos' }, likeCount: 54, commentCount: 7, likedByMe: false, }, { id: 4, - userId: 5, + userId: '5', caption: 'Wonderland Trail FKT attempt kit. Shaved 2 lbs from last year. Going for sub-24 hours this time.', images: [], createdAt: '2024-07-20T11:00:00Z', updatedAt: '2024-07-20T11:00:00Z', - author: { id: 5, firstName: 'Alex', lastName: 'Park' }, + author: { id: '5', firstName: 'Alex', lastName: 'Park' }, likeCount: 211, commentCount: 38, likedByMe: true, }, { id: 5, - userId: 6, + userId: '6', caption: "Zion Narrows overnighter loadout. Waterproof everything! Learned from experience that the river doesn't care about your gear.", images: [], createdAt: '2024-07-18T08:30:00Z', updatedAt: '2024-07-18T08:30:00Z', - author: { id: 6, firstName: 'Jordan', lastName: 'Kim' }, + author: { id: '6', firstName: 'Jordan', lastName: 'Kim' }, likeCount: 77, commentCount: 14, likedByMe: false, diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 45aa9ae1b4..cc008a5872 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -147,14 +147,14 @@ export type CatalogListResponse = { // ── Feed ───────────────────────────────────────────────────────────────────── export type PostAuthor = { - id: number; + id: string; firstName: string | null; lastName: string | null; }; export type Post = { id: number; - userId: number; + userId: string; caption: string | null; images: string[]; // array of image URLs createdAt: string; diff --git a/packages/app/src/entities/user/index.ts b/packages/app/src/entities/user/index.ts index 0495808d5b..5ab0d89883 100644 --- a/packages/app/src/entities/user/index.ts +++ b/packages/app/src/entities/user/index.ts @@ -1,7 +1,5 @@ export { useCurrentUser, - useLoginMutation, - useRegisterMutation, useUpdateProfileMutation, useUserProfile, } from './queries'; diff --git a/packages/app/src/entities/user/queries.ts b/packages/app/src/entities/user/queries.ts index 66d773a5e4..d621048b60 100644 --- a/packages/app/src/entities/user/queries.ts +++ b/packages/app/src/entities/user/queries.ts @@ -6,7 +6,7 @@ export function useCurrentUser() { return useQuery({ queryKey: queryKeys.user, queryFn: async () => { - const { data, error } = await client.auth.me.get(); + const { data, error } = await client.user.profile.get(); if (error) throw new Error('Failed to fetch current user'); return data; }, @@ -25,33 +25,6 @@ export function useUserProfile() { }); } -export function useLoginMutation() { - const client = useApiClient(); - return useMutation({ - mutationFn: async (body: { email: string; password: string }) => { - const { data, error } = await client.auth.login.post(body); - if (error) throw new Error('Login failed'); - return data; - }, - }); -} - -export function useRegisterMutation() { - const client = useApiClient(); - return useMutation({ - mutationFn: async (body: { - email: string; - password: string; - firstName?: string; - lastName?: string; - }) => { - const { data, error } = await client.auth.register.post(body); - if (error) throw new Error('Registration failed'); - return data; - }, - }); -} - interface UpdateProfileInput { firstName?: string; lastName?: string; From fdbc495fcafae839d60aa212f32a8b171924da3a Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 08:59:42 -0600 Subject: [PATCH 7/9] fix(types): annotate safe-casts and remove redundant Promise casts - admin api.ts: add safe-cast annotations on Eden Treaty PaginatedResponse double casts; res.json() returns Promise so no explicit cast needed - web auth page: drop superfluous `as Promise` on res.json() Co-Authored-By: Claude --- apps/admin/lib/api.ts | 8 ++++---- apps/web/app/auth/page.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/admin/lib/api.ts b/apps/admin/lib/api.ts index 19fd3ce416..040075d7dd 100644 --- a/apps/admin/lib/api.ts +++ b/apps/admin/lib/api.ts @@ -103,7 +103,7 @@ export async function getUsers({ query: { limit, offset, q, includeDeleted: includeDeleted ? 'true' : undefined }, }); if (error) throwOnError(error); - return unwrap(data, 'users') as unknown as PaginatedResponse; + return unwrap(data, 'users') as unknown as PaginatedResponse; // safe-cast: Eden Treaty infers wide union; runtime shape matches PaginatedResponse } export async function deleteUser(id: number): Promise<{ success: boolean }> { @@ -158,7 +158,7 @@ export async function getPacks({ query: { limit, offset, q, includeDeleted: includeDeleted ? 'true' : undefined }, }); if (error) throwOnError(error); - return unwrap(data, 'packs') as unknown as PaginatedResponse; + return unwrap(data, 'packs') as unknown as PaginatedResponse; // safe-cast: Eden Treaty infers wide union; runtime shape matches PaginatedResponse } export async function deletePack(id: string): Promise<{ success: boolean }> { @@ -219,7 +219,7 @@ export async function getCatalogItems({ query: { limit, offset, q }, }); if (error) throwOnError(error); - return unwrap(data, 'catalog') as unknown as PaginatedResponse; + return unwrap(data, 'catalog') as unknown as PaginatedResponse; // safe-cast: Eden Treaty infers wide union; runtime shape matches PaginatedResponse } export async function deleteCatalogItem(id: number): Promise<{ success: boolean }> { @@ -380,7 +380,7 @@ export async function deleteTrailCondition(reportId: string): Promise<{ success: async function adminFetch(path: string, init?: RequestInit): Promise { const res = await adminFetcher(`${API_BASE}/api/admin${path}`, init); if (!res.ok) throw new Error(`Admin API error: ${res.status}`); - return res.json() as Promise; + return res.json(); } export function resetStuckEtlJobs(): Promise<{ reset: number; ids: string[] }> { diff --git a/apps/web/app/auth/page.tsx b/apps/web/app/auth/page.tsx index 737937334b..d47266cf9d 100644 --- a/apps/web/app/auth/page.tsx +++ b/apps/web/app/auth/page.tsx @@ -37,7 +37,7 @@ function useRegisterMutation() { body: JSON.stringify({ email: body.email, password: body.password, name }), }); if (!res.ok) throw new Error('Registration failed'); - return res.json() as Promise; + return res.json(); }, }); } From 90b4999b9ad4fbda32d8aae850a4491575c69097 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 09:31:49 -0600 Subject: [PATCH 8/9] fix(expo/auth): fix TS errors blocking CI checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserSchema.id: z.number() → z.string() (Better Auth uses UUID strings; aligns with Drizzle schema's text('id') PK) - mapToUser/applySessionUser: add missing emailVerified/createdAt/updatedAt fields; replace `as` casts with asString/asBoolean guards from @packrat/guards - useAuthActions signInWithGoogle: replace dead apiClient/setToken/UserSchema references with authClient.signIn.social - ItemReviews: guard nullable review.title/text/date per CatalogItemSchema --- .../features/auth/hooks/useAuthActions.ts | 31 +++++++++---------- apps/expo/features/auth/hooks/useAuthInit.ts | 21 +++++++------ .../catalog/components/ItemReviews.tsx | 18 +++++------ packages/api/src/schemas/users.ts | 2 +- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/expo/features/auth/hooks/useAuthActions.ts b/apps/expo/features/auth/hooks/useAuthActions.ts index 6cfefe8819..95ed336fae 100644 --- a/apps/expo/features/auth/hooks/useAuthActions.ts +++ b/apps/expo/features/auth/hooks/useAuthActions.ts @@ -1,3 +1,4 @@ +import { asBoolean, asString } from '@packrat/guards'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { GoogleSignin, @@ -32,15 +33,18 @@ function redirect(route: string) { } function mapToUser(raw: Record): User { - const name = String(raw.name ?? ''); + const name = asString(raw.name) ?? ''; const spaceIdx = name.indexOf(' '); return { - id: String(raw.id ?? ''), - email: String(raw.email ?? ''), + id: asString(raw.id) ?? '', + email: asString(raw.email) ?? '', firstName: spaceIdx >= 0 ? name.slice(0, spaceIdx) : name, lastName: spaceIdx >= 0 ? name.slice(spaceIdx + 1) : '', - role: (raw.role as 'USER' | 'ADMIN') ?? 'USER', - avatarUrl: (raw.image as string | null) ?? null, + role: asString(raw.role) ?? 'USER', + emailVerified: asBoolean(raw.emailVerified) ?? null, + avatarUrl: asString(raw.image) ?? null, + createdAt: asString(raw.createdAt) ?? null, + updatedAt: asString(raw.updatedAt) ?? null, preferredWeightUnit: (raw.preferredWeightUnit as User['preferredWeightUnit']) ?? 'g', }; } @@ -88,17 +92,12 @@ export function useAuthActions() { if (!idToken) throw new Error(t('auth.noIdTokenFromGoogle')); - const { data, error } = await apiClient.auth.google.post({ idToken }); - if (error || !data) { - throw new Error(extractAuthError(error?.value, t('auth.failedToSignInWithGoogle'))); - } - - await setToken(data.accessToken); - await setRefreshToken(data.refreshToken); - userStore.set(UserSchema.parse(data.user)); - - setNeedsReauth(false); - redirect(redirectTo); + const { data, error } = await authClient.signIn.social({ + provider: 'google', + idToken: { token: idToken }, + }); + if (error) throw new Error(error.message ?? t('auth.failedToSignInWithGoogle')); + if (data && 'user' in data && data.user) applySession(data.user as Record); // safe-cast: Better Auth user type omits additionalFields; role/preferredWeightUnit present at runtime } catch (error) { setIsLoading(false); diff --git a/apps/expo/features/auth/hooks/useAuthInit.ts b/apps/expo/features/auth/hooks/useAuthInit.ts index 0aa8653852..b8e9714bc1 100644 --- a/apps/expo/features/auth/hooks/useAuthInit.ts +++ b/apps/expo/features/auth/hooks/useAuthInit.ts @@ -1,5 +1,6 @@ import { when } from '@legendapp/state'; import { clientEnvs } from '@packrat/env/expo-client'; +import { asBoolean, asString } from '@packrat/guards'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { GoogleSignin } from '@react-native-google-signin/google-signin'; import { userStore, userSyncState } from 'expo-app/features/auth/store'; @@ -23,17 +24,17 @@ async function runVersionGateMigration() { } function applySessionUser(sessionUser: Record) { + const name = asString(sessionUser.name) ?? ''; userStore.set({ - id: String(sessionUser.id ?? ''), - email: String(sessionUser.email ?? ''), - firstName: String(sessionUser.name ?? '').split(' ')[0] ?? '', - lastName: - String(sessionUser.name ?? '') - .split(' ') - .slice(1) - .join(' ') ?? '', - role: (sessionUser.role as 'USER' | 'ADMIN') ?? 'USER', // safe-cast: Better Auth client type omits additionalFields; role is present at runtime - avatarUrl: (sessionUser.image as string | null) ?? null, + id: asString(sessionUser.id) ?? '', + email: asString(sessionUser.email) ?? '', + firstName: name.split(' ')[0] ?? '', + lastName: name.split(' ').slice(1).join(' ') ?? '', + role: asString(sessionUser.role) ?? 'USER', + emailVerified: asBoolean(sessionUser.emailVerified) ?? null, + avatarUrl: asString(sessionUser.image) ?? null, + createdAt: asString(sessionUser.createdAt) ?? null, + updatedAt: asString(sessionUser.updatedAt) ?? null, preferredWeightUnit: 'g', }); } diff --git a/apps/expo/features/catalog/components/ItemReviews.tsx b/apps/expo/features/catalog/components/ItemReviews.tsx index ca3b205968..382f88ef49 100644 --- a/apps/expo/features/catalog/components/ItemReviews.tsx +++ b/apps/expo/features/catalog/components/ItemReviews.tsx @@ -44,12 +44,13 @@ export function ItemReviews({ reviews }: ItemReviewsProps) { - {reviews.map((review) => { - const isExpanded = expandedReviews[review.title] || false; - const shouldTruncate = review.text.length > 150; + {reviews.map((review, idx) => { + const reviewKey = review.title ?? String(idx); + const isExpanded = expandedReviews[reviewKey] || false; + const shouldTruncate = (review.text?.length ?? 0) > 150; return ( - + {review.user_avatar ? ( @@ -63,7 +64,9 @@ export function ItemReviews({ reviews }: ItemReviewsProps) { {review.user_name || t('catalog.anonymous')} - {formatDate(review.date)} + {review.date && ( + {formatDate(review.date)} + )} @@ -97,10 +100,7 @@ export function ItemReviews({ reviews }: ItemReviewsProps) { {shouldTruncate && ( - toggleReviewExpansion(review.title)} - > + toggleReviewExpansion(reviewKey)}> {isExpanded ? t('catalog.showLess') : t('catalog.readMore')} diff --git a/packages/api/src/schemas/users.ts b/packages/api/src/schemas/users.ts index 479d4c0f42..a1c1f15bba 100644 --- a/packages/api/src/schemas/users.ts +++ b/packages/api/src/schemas/users.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; // Base user schema export const UserSchema = z.object({ - id: z.number().int().positive(), + id: z.string(), email: z.string().email(), firstName: z.string().nullable(), lastName: z.string().nullable(), From 5676ae53c04463ab6aa5b83b67d2f8ff13f0f956 Mon Sep 17 00:00:00 2001 From: Andrew Bierman Date: Tue, 12 May 2026 10:04:10 -0600 Subject: [PATCH 9/9] chore(deps): pin @packrat-ai/nativewindui to 2.0.3-2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes 3 TypeScript errors in nativewindui source: - Icon/types: SymbolViewPropsWithStringName satisfies IconMapper constraint - AdaptiveSearchHeader, LargeTitleHeader: map 'systemDefault' autoCapitalize to 'none' Also widens expo-router peer dep to >=6.0.23 (was ~6.0.23) so bun does not install expo-router@6.0.23 into apps/expo/node_modules, which was causing TypeScript to resolve to the old API and produce 1184+ false positives. 2.0.3-2 is an exact pin; the root override prevents range resolution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bun.lock | 28 +++------------------------- package.json | 2 +- packages/ui/package.json | 2 +- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/bun.lock b/bun.lock index 67ed5ceefb..cfb54dacbe 100644 --- a/bun.lock +++ b/bun.lock @@ -638,7 +638,7 @@ "name": "@packrat/ui", "version": "2.0.25", "dependencies": { - "@packrat-ai/nativewindui": "^2.0.5", + "@packrat-ai/nativewindui": "2.0.3-2", }, }, "packages/units": { @@ -718,7 +718,7 @@ "@sentry/cli", ], "overrides": { - "@packrat-ai/nativewindui": "2.0.3", + "@packrat-ai/nativewindui": "2.0.3-2", "@sinclair/typebox": "^0.34.15", "elysia": "^1.4.0", "expo-sqlite": "~55.0.15", @@ -1475,7 +1475,7 @@ "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], - "@packrat-ai/nativewindui": ["@packrat-ai/nativewindui@2.0.3", "https://npm.pkg.github.com/download/@packrat-ai/nativewindui/2.0.3/6d1d9364d32c4a145cc9fc49a73744b553db5e14", { "peerDependencies": { "@expo/vector-icons": ">=15.0.0", "@gorhom/bottom-sheet": "^5.1.2", "@react-native-community/datetimepicker": "^8.4.0", "@react-native-community/slider": "^5.0.0", "@react-native-picker/picker": "^2.11.0", "@react-native-segmented-control/segmented-control": "^2.5.0", "@react-navigation/drawer": "^7.1.1", "@react-navigation/elements": "^2.3.1", "@react-navigation/native": "^7.0.14", "@rn-primitives/alert-dialog": "^1.1.0", "@rn-primitives/avatar": "^1.0.4", "@rn-primitives/checkbox": "^1.1.0", "@rn-primitives/context-menu": "^1.1.0", "@rn-primitives/dropdown-menu": "^1.1.0", "@rn-primitives/hooks": "^1.1.0", "@rn-primitives/portal": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@shopify/flash-list": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo-blur": "~15.0.8", "expo-device": "~8.0.0", "expo-glass-effect": "*", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linear-gradient": "~15.0.8", "expo-navigation-bar": "~5.0.10", "expo-router": "~6.0.23", "expo-symbols": "~1.0.8", "nativewind": "^4.2.3", "react": ">=19.0.0", "react-native": ">=0.79.0", "react-native-keyboard-controller": "^1.16.7", "react-native-reanimated": ">=3.17.0", "react-native-safe-area-context": ">=5.4.0", "react-native-screens": ">=4.11.0", "react-native-uitextview": "^1.1.4", "rn-icon-mapper": "^0.0.1", "tailwind-merge": "^2.2.1" } }, "sha512-C0PzEmKNqc7KS6DcxRN6hOtyIpkK6xGLO0I3x6eKBtunGSPseQWz/JEdsQW2i42xRVZN3r83loYb3v7dVUDTFg=="], + "@packrat-ai/nativewindui": ["@packrat-ai/nativewindui@2.0.3-2", "https://npm.pkg.github.com/download/@packrat-ai/nativewindui/2.0.3-2/cae289eeb09d41ea394e5273bc4694596e3facc3", { "peerDependencies": { "@expo/vector-icons": ">=15.0.0", "@gorhom/bottom-sheet": "^5.1.2", "@react-native-community/datetimepicker": "^8.4.0", "@react-native-community/slider": "^5.0.0", "@react-native-picker/picker": "^2.11.0", "@react-native-segmented-control/segmented-control": "^2.5.0", "@react-navigation/drawer": "^7.1.1", "@react-navigation/elements": "^2.3.1", "@react-navigation/native": "^7.0.14", "@rn-primitives/alert-dialog": "^1.1.0", "@rn-primitives/avatar": "^1.0.4", "@rn-primitives/checkbox": "^1.1.0", "@rn-primitives/context-menu": "^1.1.0", "@rn-primitives/dropdown-menu": "^1.1.0", "@rn-primitives/hooks": "^1.1.0", "@rn-primitives/portal": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@shopify/flash-list": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo-blur": "~15.0.8", "expo-device": "~8.0.0", "expo-glass-effect": "*", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linear-gradient": "~15.0.8", "expo-navigation-bar": "~5.0.10", "expo-router": ">=6.0.23", "expo-symbols": "~1.0.8", "nativewind": "^4.2.3", "react": ">=19.0.0", "react-native": ">=0.79.0", "react-native-keyboard-controller": "^1.16.7", "react-native-reanimated": ">=3.17.0", "react-native-safe-area-context": ">=5.4.0", "react-native-screens": ">=4.11.0", "react-native-uitextview": "^1.1.4", "rn-icon-mapper": "^0.0.1", "tailwind-merge": "^2.2.1" } }, "sha512-gimhxLYi3IiAqV4h0s1pzPb4mxy07XOcgRkhN8nOVRi7t6Ucb5dOGv2ArWY3WfQlSrN52dPRxzDtdsnIn33FRg=="], "@packrat/analytics": ["@packrat/analytics@workspace:packages/analytics"], @@ -4657,14 +4657,6 @@ "@modelcontextprotocol/sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - "@packrat-ai/nativewindui/expo-device": ["expo-device@55.0.15", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-vXy4U/IeYI+zHGG45Ap6J7EuyQmkstyo8I+/5YGr5q2zmqLBo6SWE62wii8i9hLHheHn6AtF9UPrSWAREJrE8A=="], - - "@packrat-ai/nativewindui/expo-image": ["expo-image@55.0.9", "", { "dependencies": { "sf-symbols-typescript": "^2.2.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-+NVgWv+tr7a6EpBEaIIVVp+XfruRA2JL5xOxvd6ajvFGdH0rOhagwX1m1piAII6w7sh6uAnBr8X+fDZsav7B2w=="], - - "@packrat-ai/nativewindui/expo-router": ["expo-router@55.0.13", "", { "dependencies": { "@expo/metro-runtime": "^55.0.10", "@expo/schema-utils": "^55.0.3", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/native": "^7.1.33", "@react-navigation/native-stack": "^7.14.5", "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^55.0.10", "expo-image": "^55.0.9", "expo-server": "^55.0.8", "expo-symbols": "^55.0.7", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-native-is-edge-to-edge": "^1.2.1", "semver": "~7.6.3", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "use-latest-callback": "^0.2.1", "vaul": "^1.1.2" }, "peerDependencies": { "@expo/log-box": "55.0.11", "@react-navigation/drawer": "^7.9.4", "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^55.0.15", "expo-linking": "^55.0.14", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-screens": "*", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@react-navigation/drawer", "@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-cIBR5RmQtbr+b535mlbMhmm7lweVZXFtjzJOgJTutoxIApRztl816kFRFNesnVyqQ0LZrEU0a6vqa3i0wdlRQw=="], - - "@packrat-ai/nativewindui/expo-symbols": ["expo-symbols@55.0.7", "", { "dependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "expo": "*", "expo-font": "*", "react": "*", "react-native": "*" } }, "sha512-y4ALLbncSGQzhFLw1PaIBbO39xzaw3ie249HmK6zK/WLJYfw4Z/9UU4iPKO3KCE4FyCKIzd+yRsvzvlri23YrQ=="], - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@poppinss/colors/kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], @@ -5341,18 +5333,6 @@ "@manypkg/tools/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@packrat-ai/nativewindui/expo-router/@expo/log-box": ["@expo/log-box@55.0.11", "", { "dependencies": { "@expo/dom-webview": "^55.0.5", "anser": "^1.4.9", "stacktrace-parser": "^0.1.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-JQHFLWkskIbJi6cxYMjErx8lQqfFJilDQLKmdTO3m3YkdmN9GE/CrzjOfVlCG0DGEGZJ90br0pGKvGPdXNsHKw=="], - - "@packrat-ai/nativewindui/expo-router/@expo/schema-utils": ["@expo/schema-utils@55.0.3", "", {}, "sha512-l9KHVjTo6MvoeyvwNr6AjckGJm8NIcqZ3QSAh51cWozXW9v2AUjyCyqYtFtyntLWRZ0x/ByYJishpQo4ZQq45Q=="], - - "@packrat-ai/nativewindui/expo-router/expo-glass-effect": ["expo-glass-effect@55.0.10", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-5kL/jATvgJWdrqPdxixrECJqD2l8cfQ4ALr1DK7qi9XkyI97ejXvUjB2VsfEePNy3Fg+/VwzA3n3L7Nv3tAPkw=="], - - "@packrat-ai/nativewindui/expo-router/expo-server": ["expo-server@55.0.8", "", {}, "sha512-AoV5TKuO4biSzrhe/OVLyInfTT0pV9/OOc/g/oVq5vmCjL8SaSYTkES8PLt+67Tm7VqX+Dn0+kSx1nQcjEKaPw=="], - - "@packrat-ai/nativewindui/expo-router/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "@packrat-ai/nativewindui/expo-router/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], - "@react-native-ai/apple/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@react-native-ai/llama/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], @@ -5815,8 +5795,6 @@ "@lhci/cli/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - "@packrat-ai/nativewindui/expo-router/@expo/log-box/@expo/dom-webview": ["@expo/dom-webview@55.0.5", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-lt3uxYOCk3wmWvtOOvsC35CKGbDAOx5C2EaY8SH1JVSfBzqmF8Cs0Xp1MPxncDPMyxpMiWx5SvvV/iLF1rJU4A=="], - "@react-native/codegen/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "@react-native/community-cli-plugin/metro/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], diff --git a/package.json b/package.json index 537658fd03..77d64f21a6 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "web": "bun run --cwd apps/web dev" }, "overrides": { - "@packrat-ai/nativewindui": "2.0.3", + "@packrat-ai/nativewindui": "2.0.3-2", "@sinclair/typebox": "^0.34.15", "elysia": "^1.4.0", "expo-sqlite": "~55.0.15", diff --git a/packages/ui/package.json b/packages/ui/package.json index 6447cfc226..16332aef81 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,6 +3,6 @@ "version": "2.0.25", "private": true, "dependencies": { - "@packrat-ai/nativewindui": "^2.0.5" + "@packrat-ai/nativewindui": "2.0.3-2" } }