5 Commits

Author SHA1 Message Date
2d71ce15af docs: add Phase 21 (Item & Catalog Detail Pages), renumber 21→22, 22→23
Some checks failed
CI / ci (push) Failing after 13s
CI / e2e (push) Has been skipped
2026-04-06 14:38:27 +02:00
4b8dec6252 docs(quick-260406-j44): comprehensive dev seed script for bikepacking gear data 2026-04-06 13:54:05 +02:00
6836790e55 docs(quick-260406-j44): complete dev seed script summary
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:53:29 +02:00
eb7f37fe28 feat(quick-260406-j44): add idempotent dev seed runner and db:seed:dev script
- Seed runner inserts user, categories, global items, tags, user items,
  threads with candidates, setups, and settings in FK order
- Idempotent: checks for dev-user-seed logtoSub before running
- Reuses seedGlobalItems() for base catalog data
- Added db:seed:dev npm script to package.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:52:45 +02:00
24f3a8a8a2 feat(quick-260406-j44): add dev seed data constants for bikepacking gear
- 10 categories, 36 global items with realistic weights/prices
- 17 user items (10 catalog-linked, 7 standalone)
- 3 threads with candidates, 2 setups, tag assignments, settings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 13:50:58 +02:00
8 changed files with 1405 additions and 17 deletions

View File

@@ -58,6 +58,14 @@ Requirements for this milestone. Each maps to roadmap phases.
- [ ] **CATFLOW-07**: Manual entry fallback when item not in catalog
- [ ] **CATFLOW-08**: Non-functional "Submit to catalog?" prompt shown after manual save
### Item & Catalog Detail Pages
- [ ] **DETAIL-01**: Clicking a collection item navigates to a full detail page (`/items/:id`) showing all item data
- [ ] **DETAIL-02**: Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button
- [ ] **DETAIL-03**: Item detail page has edit mode toggle for modifying personal fields (notes, category, quantity, purchase price)
- [ ] **DETAIL-04**: Thread candidates navigate to detail pages instead of opening slide-out panels
- [ ] **DETAIL-05**: Slide-out panels for items and candidates are removed from the application
### Tags
- [x] **TAG-01**: Tags table seeded with curated tag set for outdoor/adventure gear
@@ -165,18 +173,23 @@ Which phases cover which requirements. Updated during roadmap creation.
| CATFLOW-01 | Phase 20 | Complete |
| CATFLOW-02 | Phase 20 | Complete |
| CATFLOW-03 | Phase 19, 21 | Pending |
| CATFLOW-03 | Phase 19, 22 | Pending |
| CATFLOW-04 | Phase 19 | Complete |
| CATFLOW-05 | Phase 19, 21 | Pending |
| CATFLOW-06 | Phase 19, 21 | Pending |
| CATFLOW-07 | Phase 22 | Pending |
| CATFLOW-08 | Phase 22 | Pending |
| CATFLOW-05 | Phase 19, 22 | Pending |
| CATFLOW-06 | Phase 19, 22 | Pending |
| CATFLOW-07 | Phase 23 | Pending |
| CATFLOW-08 | Phase 23 | Pending |
| TAG-01 | Phase 19 | Complete |
| TAG-02 | Phase 19 | Complete |
| DETAIL-01 | Phase 21 | Pending |
| DETAIL-02 | Phase 21 | Pending |
| DETAIL-03 | Phase 21 | Pending |
| DETAIL-04 | Phase 21 | Pending |
| DETAIL-05 | Phase 21 | Pending |
**Coverage:**
- v2.0 requirements: 40 total
- Mapped to phases: 40
- v2.0 requirements: 45 total
- Mapped to phases: 45
- Unmapped: 0
---

View File

@@ -6,7 +6,7 @@
-**v1.1 Fixes & Polish** — Phases 4-6 (shipped 2026-03-15)
-**v1.2 Collection Power-Ups** — Phases 7-9 (shipped 2026-03-16)
- 🚧 **v1.3 Research & Decision Tools** — Phases 10-13 (in progress)
- 📋 **v2.0 Platform Foundation** <20><> Phases 14-22 (planned)
- 📋 **v2.0 Platform Foundation** <20><> Phases 14-23 (planned)
## Phases
@@ -57,8 +57,9 @@
- [x] **Phase 18: Global Items & Public Profiles** — Global item catalog, user profiles, and public setup sharing (completed 2026-04-05)
- [x] **Phase 19: Reference Item Model & Tags Schema** — Collection items as references to global catalog, tag system for discovery (completed 2026-04-05)
- [x] **Phase 20: FAB & Full-Screen Catalog Search** — Global FAB with mini menu, full-screen catalog search with tag filtering (completed 2026-04-06)
- [ ] **Phase 21: Add-from-Catalog & Thread Integration** — Add catalog items to collection and threads, resolution creates reference items
- [ ] **Phase 22: Manual Entry Fallback** — Manual add for items not in catalog, non-functional submission prompt
- [ ] **Phase 21: Item & Catalog Detail Pages** — Full detail pages for collection items and catalog entries, replacing slide-out panels
- [ ] **Phase 22: Add-from-Catalog & Thread Integration** — Add catalog items to collection and threads, resolution creates reference items
- [ ] **Phase 23: Manual Entry Fallback** — Manual add for items not in catalog, non-functional submission prompt
## Phase Details
@@ -206,9 +207,22 @@ Plans:
- [x] 20-02-PLAN.md — FabMenu component, CatalogSearchOverlay component, root layout wiring
**UI hint**: yes
### Phase 21: Add-from-Catalog & Thread Integration
**Goal**: Users can add catalog items to their collection and to threads directly from search
### Phase 21: Item & Catalog Detail Pages
**Goal**: Collection items and catalog entries have full detail pages, replacing the slide-out panel pattern
**Depends on**: Phase 20
**Requirements**: DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04, DETAIL-05
**Success Criteria** (what must be TRUE):
1. Clicking a collection item card navigates to `/items/:id` showing full item details with edit toggle
2. Clicking a catalog search result card navigates to `/global-items/:id` showing public catalog details with "Add to Collection" button
3. Thread candidates navigate to detail pages instead of opening slide-out panels
4. Item slide-out panel and candidate slide-out panel are removed from the root layout
5. No visual distinction between reference items and standalone items — same layout, some fields may be empty
**Plans**: TBD
**UI hint**: yes
### Phase 22: Add-from-Catalog & Thread Integration
**Goal**: Users can add catalog items to their collection and to threads directly from search
**Depends on**: Phase 21
**Requirements**: CATFLOW-03, CATFLOW-05, CATFLOW-06
**Success Criteria** (what must be TRUE):
1. User can add a catalog item to collection with one confirmation step (category picker + notes)
@@ -217,9 +231,9 @@ Plans:
**Plans**: TBD
**UI hint**: yes
### Phase 22: Manual Entry Fallback
### Phase 23: Manual Entry Fallback
**Goal**: Users can still add items not found in the catalog via manual entry
**Depends on**: Phase 21
**Depends on**: Phase 22
**Requirements**: CATFLOW-07, CATFLOW-08
**Success Criteria** (what must be TRUE):
1. User can fall back to manual entry from catalog search via "Add Manually" link
@@ -252,5 +266,6 @@ Plans:
| 18. Global Items & Public Profiles | v2.0 | 4/5 | Complete | 2026-04-05 |
| 19. Reference Item Model & Tags Schema | v2.0 | 3/3 | Complete | 2026-04-05 |
| 20. FAB & Full-Screen Catalog Search | v2.0 | 2/2 | Complete | 2026-04-06 |
| 21. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
| 22. Manual Entry Fallback | v2.0 | 0/? | Not started | - |
| 21. Item & Catalog Detail Pages | v2.0 | 0/? | Not started | - |
| 22. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
| 23. Manual Entry Fallback | v2.0 | 0/? | Not started | - |

View File

@@ -63,6 +63,12 @@ Key decisions made during v2.0 planning:
None active.
### Quick Tasks Completed
| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
| 260406-j44 | Comprehensive dev seed script for bikepacking gear data | 2026-04-06 | — | [260406-j44-comprehensive-dev-seed-script-for-bikepa](./quick/260406-j44-comprehensive-dev-seed-script-for-bikepa/) |
### Blockers/Concerns
- Auth provider decision (Logto vs Authentik) must be resolved before Phase 15 planning

View File

@@ -0,0 +1,204 @@
---
phase: quick
plan: 260406-j44
type: execute
wave: 1
depends_on: []
files_modified:
- src/db/dev-seed.ts
- src/db/dev-seed-data.ts
- package.json
autonomous: true
requirements: []
must_haves:
truths:
- "Running `bun run db:seed:dev` populates the database with realistic bikepacking gear data"
- "Re-running the script on a populated DB is safe (idempotent — skips if data exists)"
- "All foreign key relationships are valid (items reference real categories, setup_items reference real items, etc.)"
artifacts:
- path: "src/db/dev-seed-data.ts"
provides: "All seed data as typed constants"
- path: "src/db/dev-seed.ts"
provides: "Idempotent seed runner"
- path: "package.json"
provides: "db:seed:dev script entry"
key_links:
- from: "src/db/dev-seed.ts"
to: "src/db/schema.ts"
via: "drizzle insert operations"
pattern: "db\\.insert\\(schema\\."
- from: "src/db/dev-seed.ts"
to: "src/db/dev-seed-data.ts"
via: "import data constants"
pattern: "import.*from.*dev-seed-data"
---
<objective>
Create a comprehensive development seed script that populates PostgreSQL with realistic bikepacking/outdoor gear data for local development.
Purpose: Enable developers to quickly stand up a fully-populated dev environment with realistic data spanning all entity types — global items, tags, user collections, threads, setups, and settings.
Output: Two new files (`dev-seed-data.ts` for data, `dev-seed.ts` for the runner) and a `db:seed:dev` npm script.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@CLAUDE.md
@src/db/schema.ts
@src/db/seed-global-items.ts
@src/db/global-items-seed.json
@tests/helpers/db.ts
@src/db/index.ts
<interfaces>
<!-- Key schema exports the seed script will use -->
From src/db/schema.ts:
- users: { id (serial PK), logtoSub (text, unique), displayName (text), avatarUrl (text), bio (text) }
- categories: { id (serial PK), name (text), icon (text), userId (int FK users) } — unique(userId, name)
- items: { id (serial PK), name, weightGrams, priceCents, categoryId (FK), userId (FK), notes, productUrl, imageFilename, imageSourceUrl, quantity (default 1), globalItemId (FK nullable), purchasePriceCents }
- threads: { id (serial PK), name, status (default "active"), resolvedCandidateId, categoryId (FK), userId (FK) }
- threadCandidates: { id (serial PK), threadId (FK cascade), name, weightGrams, priceCents, categoryId (FK), notes, productUrl, imageFilename, imageSourceUrl, status (default "researching"), pros, cons, sortOrder, globalItemId (FK nullable) }
- setups: { id (serial PK), name, userId (FK), isPublic (default false) }
- setupItems: { id (serial PK), setupId (FK cascade), itemId (FK cascade), classification (default "base") }
- globalItems: { id (serial PK), brand, model, category, weightGrams, priceCents, imageUrl, description }
- tags: { id (serial PK), name (unique) }
- globalItemTags: { globalItemId (FK cascade), tagId (FK cascade) } — composite PK
- settings: { userId (FK), key (text), value (text) } — composite PK(userId, key)
From src/db/index.ts:
- db: drizzle instance connected to PostgreSQL via DATABASE_URL
From src/db/seed-global-items.ts (idempotent pattern to follow):
- Check `existing.length > 0` before inserting
- Accept optional `db` parameter with production db as default
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create seed data constants</name>
<files>src/db/dev-seed-data.ts</files>
<action>
Create `src/db/dev-seed-data.ts` exporting typed constant arrays for all seed data. Keep data and logic separate for maintainability.
**Categories** (8-10 with icons):
- Bags (briefcase), Shelter (tent), Sleep System (moon), Cooking (flame), Lighting (flashlight), Tools & Repair (wrench), Clothing (shirt), Water (droplets), Electronics (battery), Navigation (compass)
**Global Items** (35-45 items spanning categories). Use real bikepacking gear brands/models with realistic weights and prices. Examples per category:
- Bags: Revelate Designs Terrapin, Apidura Expedition Handlebar Pack, Ortlieb Frame-Pack, Rockgeist BarJam, Oveja Negra Superwedgie
- Shelter: Zpacks Duplex, Tarptent Stratospire, Durston X-Mid 1, Big Agnes Copper Spur HV UL1
- Sleep: Enlightened Equipment Enigma 20, Therm-a-Rest NeoAir XLite, Nemo Tensor Insulated, Sea to Summit Aeros Pillow
- Cooking: BRS-3000T, Soto Windmaster, Toaks 750ml, Snow Peak Ti-Mini Solo
- Lighting: Nitecore NU25, Lezyne Lite Drive, Fenix HL60R
- Tools: Park Tool IB-3, Lezyne CNC Chain Breaker, Gorilla Tape mini roll
- Clothing: Patagonia R1 Air, Frogg Toggs UL2 Rain Suit, Buff Merino Wool
- Water: Sawyer Squeeze, Katadyn BeFree, HydraPak Seeker 2L
- Electronics: Anker 10000 PD, Garmin inReach Mini 2
- Navigation: Wahoo ELEMNT BOLT, Caltopo printed maps holder
Each global item: `{ brand, model, category, weightGrams, priceCents, description }` — use integers for priceCents (e.g., 67900 for $679.00).
**Tags** (reuse existing SEED_TAGS from seed-global-items.ts plus a few more):
- Existing tags are already seeded by `seedTags()`, so do NOT re-seed them.
**Tag assignments** — array of `{ globalItemIndex, tagNames }` mapping global items to tags by index position and tag name. Example: index 0 (Terrapin) gets ["saddlebag", "waterproof", "bikepacking"]. Assign 2-4 tags per global item.
**User items** (15-20 items for the dev user's collection):
- ~10 items linked to global items via `globalItemId` (reference items). Use the index into the global items array. Include `purchasePriceCents` for some (actual price paid, sometimes different from MSRP).
- ~5-7 standalone items with no globalItemId (custom/unique gear the user added manually)
- Spread across categories. Include notes on some, quantity > 1 on a couple.
**Threads** (3 threads):
1. Active thread "Handlebar Bag Upgrade" in Bags category with 3 candidates (some catalog-linked via globalItemId index), statuses: researching/shortlisted/researching
2. Active thread "Navigation Computer" in Electronics with 2 candidates
3. Resolved thread "Camp Stove" in Cooking with 2 candidates, one marked "arrived"
**Setups** (2 setups):
1. "Weekend Overnighter" — 6-8 items from the user's collection, mix of base/worn/consumable classifications, isPublic: true
2. "Ultra-Light Day Ride" — 3-4 items, isPublic: false
**Settings**: weightUnit "g", currency "EUR"
Export everything as named constants with proper TypeScript types (using `as const` where appropriate for literal inference).
</action>
<verify>
<automated>bunx tsc --noEmit src/db/dev-seed-data.ts 2>&1 | head -20</automated>
</verify>
<done>All seed data constants exported with proper types, no TypeScript errors</done>
</task>
<task type="auto">
<name>Task 2: Create seed runner and wire npm script</name>
<files>src/db/dev-seed.ts, package.json</files>
<action>
Create `src/db/dev-seed.ts` — the idempotent seed runner.
**Structure:**
1. Import `db` from `./index.ts`, all schema tables from `./schema.ts`, all data from `./dev-seed-data.ts`, and `seedGlobalItems` from `./seed-global-items.ts`
2. Main `seedDevData()` async function:
**Idempotency check:** Query `users` table for a user with `logtoSub = "dev-user-seed"`. If found, log "Dev seed data already exists, skipping." and return early. This single check gates the entire script since all other data depends on this user.
**Insertion order** (respects FK constraints):
1. Call `seedGlobalItems(db)` first — reuse existing function to populate global_items and tags
2. Insert dev user: `{ logtoSub: "dev-user-seed", displayName: "Dev User", bio: "Bikepacking enthusiast" }` — capture returned user for userId FK
3. Insert categories (with userId from step 2) — capture returned rows for categoryId mapping
4. Look up tag IDs by name from tags table (they were seeded in step 1) — build `tagNameToId` map
5. Insert global item tag assignments using `globalItemTags` table — use the global item IDs from step 1 and tag IDs from step 4. Note: `seedGlobalItems` does not return IDs, so query `globalItems` table to get IDs by brand+model matching after seeding.
6. Insert user items (with userId, categoryId, globalItemId references) — capture returned rows for setup_items
7. Insert threads (with userId, categoryId) — capture returned IDs
8. Insert thread candidates (with threadId, categoryId, globalItemId) — for the resolved thread, set `resolvedCandidateId` on the thread after inserting candidates
9. Insert setups (with userId) — capture returned IDs
10. Insert setup_items (with setupId, itemId references from step 6)
11. Insert settings (with userId)
12. Log summary: "Dev seed complete: X global items, X tags, X user items, X threads, X setups"
**Script entry point:** At bottom of file:
```typescript
seedDevData()
.then(() => process.exit(0))
.catch((err) => { console.error("Seed failed:", err); process.exit(1); });
```
**package.json:** Add `"db:seed:dev": "bun run src/db/dev-seed.ts"` to scripts section.
Use `db.insert(...).values(...).returning()` to capture IDs needed for FK references. Use `db.insert(...).values(...)` (no returning) for leaf tables.
Wrap everything in a try/catch. On error, log the error and re-throw. Do NOT use transactions (PostgreSQL serial IDs and the idempotency check make this safe enough for dev seeding).
</action>
<verify>
<automated>bunx tsc --noEmit src/db/dev-seed.ts 2>&1 | head -20</automated>
</verify>
<done>
- `bun run db:seed:dev` executes without error on a fresh (migrated) database
- Re-running logs "already exists, skipping" and exits cleanly
- Database contains: 1 dev user, 8+ categories, 35+ global items with tag assignments, 15+ user items (some catalog-linked), 3 threads with candidates, 2 setups with items, settings
</done>
</task>
</tasks>
<verification>
1. `bunx tsc --noEmit src/db/dev-seed-data.ts src/db/dev-seed.ts` — no type errors
2. `bun run db:seed:dev` — completes without error (requires running PostgreSQL)
3. Re-run `bun run db:seed:dev` — logs skip message, exits 0
</verification>
<success_criteria>
- Script populates all entity types: users, categories, global items, tags, global item tag links, items (reference + standalone), threads, candidates (some catalog-linked), setups, setup_items, settings
- Idempotent: safe to run multiple times
- Data is realistic bikepacking gear with accurate weights/prices
- Foreign key relationships are all valid
- `bun run db:seed:dev` is the only command needed
</success_criteria>
<output>
After completion, create `.planning/quick/260406-j44-comprehensive-dev-seed-script-for-bikepa/260406-j44-SUMMARY.md`
</output>

View File

@@ -0,0 +1,64 @@
---
phase: quick
plan: 260406-j44
subsystem: database
tags: [seed-data, dev-tooling, bikepacking]
dependency_graph:
requires: [src/db/schema.ts, src/db/seed-global-items.ts, src/db/index.ts]
provides: [src/db/dev-seed.ts, src/db/dev-seed-data.ts, db:seed:dev script]
affects: [local development workflow]
tech_stack:
added: []
patterns: [idempotent seed script, FK-ordered insertion, data/logic separation]
key_files:
created: [src/db/dev-seed-data.ts, src/db/dev-seed.ts]
modified: [package.json]
decisions:
- Separated data constants (dev-seed-data.ts) from runner logic (dev-seed.ts) for maintainability
- Used logtoSub="dev-user-seed" as idempotency sentinel — single check gates entire script
- Global items are deduplicated by brand+model to avoid conflicts with existing seed-global-items.json data
- Tag assignments skip existing links to stay idempotent at the row level
metrics:
duration: ~4m
completed: 2026-04-06
tasks_completed: 2
tasks_total: 2
files_created: 2
files_modified: 1
---
# Quick Task 260406-j44: Comprehensive Dev Seed Script Summary
Idempotent dev seed script populating PostgreSQL with 36 global items, 30 tag assignments, 10 categories, 17 user items (10 catalog-linked + 7 standalone), 3 threads with 7 candidates, 2 setups, and user settings for realistic bikepacking gear development.
## Commits
| # | Hash | Message |
|---|------|---------|
| 1 | 24f3a8a | feat(quick-260406-j44): add dev seed data constants for bikepacking gear |
| 2 | eb7f37f | feat(quick-260406-j44): add idempotent dev seed runner and db:seed:dev script |
## Task Details
### Task 1: Create seed data constants
Created `src/db/dev-seed-data.ts` with typed constant arrays covering all entity types. Data uses real bikepacking gear brands (Revelate Designs, Zpacks, Enlightened Equipment, Nitecore, etc.) with accurate weights and prices in cents. Includes a `categoryDisplayName()` helper for mapping seed category slugs to display names.
### Task 2: Create seed runner and wire npm script
Created `src/db/dev-seed.ts` with FK-ordered insertion (users -> categories -> global items -> tags -> items -> threads -> candidates -> setups -> setup_items -> settings). Idempotency check queries for `logtoSub = "dev-user-seed"`. Global items are deduplicated against existing `seed-global-items.json` data by brand+model. Added `db:seed:dev` script to package.json.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Added undefined guards on .returning() results**
- **Found during:** Task 2 type-check
- **Issue:** TypeScript strict mode flagged `.returning()` destructured results as possibly undefined
- **Fix:** Added `if (!result) throw new Error(...)` guards after each `.returning()` call
- **Files modified:** src/db/dev-seed.ts
- **Commit:** eb7f37f
## Known Stubs
None — all data is fully wired with valid FK references.
## Self-Check: PASSED

View File

@@ -13,7 +13,8 @@
"test": "bun test tests/",
"test:e2e": "bunx playwright test",
"test:e2e:ui": "bunx playwright test --ui",
"lint": "bunx @biomejs/biome check ."
"lint": "bunx @biomejs/biome check .",
"db:seed:dev": "bun run src/db/dev-seed.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.4.7",

782
src/db/dev-seed-data.ts Normal file
View File

@@ -0,0 +1,782 @@
// ── Dev Seed Data ──────────────────────────────────────────────────
// All seed data as typed constants for local development.
// Realistic bikepacking gear with accurate weights and prices.
// ── Categories ─────────────────────────────────────────────────────
export const DEV_CATEGORIES = [
{ name: "Bags", icon: "briefcase" },
{ name: "Shelter", icon: "tent" },
{ name: "Sleep System", icon: "moon" },
{ name: "Cooking", icon: "flame" },
{ name: "Lighting", icon: "flashlight" },
{ name: "Tools & Repair", icon: "wrench" },
{ name: "Clothing", icon: "shirt" },
{ name: "Water", icon: "droplets" },
{ name: "Electronics", icon: "battery" },
{ name: "Navigation", icon: "compass" },
] as const;
// ── Global Items ───────────────────────────────────────────────────
// Index positions are referenced by user items, thread candidates, and tag assignments.
export const DEV_GLOBAL_ITEMS = [
// Bags (indices 0-5)
{
brand: "Revelate Designs",
model: "Terrapin System",
category: "bags",
weightGrams: 529,
priceCents: 18500,
description:
"Waterproof saddle bag with 14L capacity, roll-top closure, and integrated seat bag mount.",
},
{
brand: "Apidura",
model: "Expedition Handlebar Pack",
category: "bags",
weightGrams: 300,
priceCents: 16000,
description:
"14L waterproof handlebar roll bag with internal dry bag and accessory pocket.",
},
{
brand: "Ortlieb",
model: "Frame-Pack RC",
category: "bags",
weightGrams: 250,
priceCents: 12000,
description:
"6L waterproof roll-closure frame bag with TIZIP zipper for full-frame bikes.",
},
{
brand: "Rockgeist",
model: "BarJam",
category: "bags",
weightGrams: 142,
priceCents: 9900,
description:
"Ultralight handlebar harness with side-loading dry bag compatibility.",
},
{
brand: "Oveja Negra",
model: "Superwedgie",
category: "bags",
weightGrams: 170,
priceCents: 7500,
description:
"Half-frame bag with easy-access zipper and internal organization.",
},
{
brand: "Apidura",
model: "Racing Top Tube Pack",
category: "bags",
weightGrams: 72,
priceCents: 5500,
description:
"Bolt-on 1L top tube pack for nutrition and essentials during racing.",
},
// Shelter (indices 6-9)
{
brand: "Zpacks",
model: "Duplex",
category: "shelter",
weightGrams: 539,
priceCents: 67900,
description:
"Dyneema Composite Fabric two-person trekking pole shelter, freestanding with optional poles.",
},
{
brand: "Tarptent",
model: "Stratospire Li",
category: "shelter",
weightGrams: 737,
priceCents: 62500,
description:
"Two-person double-wall tent in Dyneema with dual vestibules and excellent ventilation.",
},
{
brand: "Durston",
model: "X-Mid 1 Solid",
category: "shelter",
weightGrams: 880,
priceCents: 22000,
description:
"Single-wall silpoly trekking pole tent with symmetrical design and two vestibules.",
},
{
brand: "Big Agnes",
model: "Copper Spur HV UL1",
category: "shelter",
weightGrams: 936,
priceCents: 42000,
description:
"Freestanding ultralight one-person tent with high-volume architecture and DAC poles.",
},
// Sleep System (indices 10-14)
{
brand: "Enlightened Equipment",
model: "Enigma 20F",
category: "sleep",
weightGrams: 567,
priceCents: 35500,
description:
"20F down quilt with 850FP DownTek water-resistant fill, sewn footbox option.",
},
{
brand: "Therm-a-Rest",
model: "NeoAir XLite NXT",
category: "sleep",
weightGrams: 354,
priceCents: 21000,
description:
"R-value 4.5 ultralight inflatable sleeping pad with ThermaCapture reflective technology.",
},
{
brand: "Nemo",
model: "Tensor Insulated Regular",
category: "sleep",
weightGrams: 425,
priceCents: 17000,
description:
"R-value 4.2 insulated sleeping pad with Spaceframe baffles for stability.",
},
{
brand: "Sea to Summit",
model: "Aeros Premium Pillow",
category: "sleep",
weightGrams: 79,
priceCents: 4500,
description:
"Brushed 50D polyester inflatable pillow with multifunctional valve.",
},
{
brand: "Western Mountaineering",
model: "NanoLite 22F",
category: "sleep",
weightGrams: 510,
priceCents: 46000,
description:
"Premium 850+ FP goose down mummy bag with continuous baffles and draft collar.",
},
// Cooking (indices 15-19)
{
brand: "BRS",
model: "BRS-3000T",
category: "cooking",
weightGrams: 25,
priceCents: 2000,
description:
"Ultralight titanium canister stove, 25g with piezo ignition, 2700W output.",
},
{
brand: "Soto",
model: "WindMaster",
category: "cooking",
weightGrams: 67,
priceCents: 7500,
description:
"Micro-regulator stove with concave burner head for excellent wind resistance.",
},
{
brand: "Toaks",
model: "Light Titanium 750ml",
category: "cooking",
weightGrams: 86,
priceCents: 3200,
description:
"Titanium pot with graduated measurements, lid, and folding bail handle.",
},
{
brand: "Snow Peak",
model: "Ti-Mini Solo Combo",
category: "cooking",
weightGrams: 198,
priceCents: 5500,
description:
"Titanium cookset with 850ml pot, lid/pan, and nesting mug for solo cooking.",
},
{
brand: "MSR",
model: "PocketRocket Deluxe",
category: "cooking",
weightGrams: 83,
priceCents: 6000,
description:
"Pressure-regulated canister stove with built-in wind clip and push-start ignition.",
},
// Lighting (indices 20-22)
{
brand: "Nitecore",
model: "NU25 UL",
category: "lighting",
weightGrams: 28,
priceCents: 3600,
description:
"Rechargeable ultralight headlamp with 400 lumens, red/high-CRI aux LEDs.",
},
{
brand: "Lezyne",
model: "Lite Drive 1200+",
category: "lighting",
weightGrams: 176,
priceCents: 8500,
description:
"1200 lumen USB-C rechargeable bike light with MOR optical lens design.",
},
{
brand: "Fenix",
model: "HL60R",
category: "lighting",
weightGrams: 134,
priceCents: 7000,
description:
"950 lumen rechargeable headlamp with neutral white LED and red light mode.",
},
// Tools & Repair (indices 23-25)
{
brand: "Park Tool",
model: "IB-3",
category: "tools",
weightGrams: 175,
priceCents: 3500,
description:
"Folding hex/Torx multi-tool with 3-6mm hex, T25, Phillips and flathead.",
},
{
brand: "Lezyne",
model: "CNC Chain Breaker",
category: "tools",
weightGrams: 28,
priceCents: 2300,
description:
"CNC-machined aluminum chain tool compatible with 8-12 speed chains.",
},
{
brand: "Gorilla Tape",
model: "Mini Duct Tape Roll",
category: "tools",
weightGrams: 30,
priceCents: 500,
description: "1-inch wide mini roll for emergency repairs, 5-yard length.",
},
// Clothing (indices 26-28)
{
brand: "Patagonia",
model: "R1 Air Full-Zip",
category: "clothing",
weightGrams: 266,
priceCents: 15900,
description:
"Breathable midlayer fleece with open-knit R1 Air fabric for high-output activities.",
},
{
brand: "Frogg Toggs",
model: "Ultra-Lite2 Rain Suit",
category: "clothing",
weightGrams: 340,
priceCents: 2500,
description:
"Budget ultralight rain jacket and pants set, DriPore breathable material.",
},
{
brand: "Buff",
model: "Merino Wool Multifunctional",
category: "clothing",
weightGrams: 43,
priceCents: 2800,
description:
"Seamless 100% merino wool neck gaiter with natural odor resistance.",
},
// Water (indices 29-31)
{
brand: "Sawyer",
model: "Squeeze SP129",
category: "water",
weightGrams: 85,
priceCents: 3500,
description:
"0.1 micron hollow-fiber water filter with high flow rate and backflush capability.",
},
{
brand: "Katadyn",
model: "BeFree 1L",
category: "water",
weightGrams: 63,
priceCents: 4500,
description:
"EZ-Clean membrane filter with collapsible Hydrapak flask, 2L/min flow rate.",
},
{
brand: "HydraPak",
model: "Seeker 2L",
category: "water",
weightGrams: 76,
priceCents: 1600,
description:
"Ultralight collapsible water storage with wide mouth and Plug-N-Play cap.",
},
// Electronics (indices 32-33)
{
brand: "Anker",
model: "Nano Power Bank 10000 PD",
category: "electronics",
weightGrams: 220,
priceCents: 3000,
description:
"10000mAh 30W USB-C PD power bank with built-in display and passthrough charging.",
},
{
brand: "Garmin",
model: "inReach Mini 2",
category: "electronics",
weightGrams: 100,
priceCents: 39999,
description:
"Satellite communicator with SOS, two-way messaging, tracking, and weather forecasts.",
},
// Navigation (indices 34-35)
{
brand: "Wahoo",
model: "ELEMNT BOLT V2",
category: "navigation",
weightGrams: 68,
priceCents: 27999,
description:
"GPS cycling computer with color display, turn-by-turn navigation, and smart trainer integration.",
},
{
brand: "Ortlieb",
model: "Ultimate Six Classic",
category: "navigation",
weightGrams: 500,
priceCents: 9000,
description:
"5L handlebar bag with integrated map case and magnetic closure, IP64 rated.",
},
] as const;
// ── Tag Assignments ────────────────────────────────────────────────
// Maps global item index -> tag names. Tags are seeded by seedGlobalItems().
export const DEV_TAG_ASSIGNMENTS = [
{ globalItemIndex: 0, tagNames: ["saddlebag", "waterproof", "bikepacking"] },
{
globalItemIndex: 1,
tagNames: ["handlebar-bag", "waterproof", "bikepacking"],
},
{
globalItemIndex: 2,
tagNames: ["framebag", "waterproof", "bikepacking", "touring"],
},
{
globalItemIndex: 3,
tagNames: ["handlebar-bag", "ultralight", "bikepacking"],
},
{ globalItemIndex: 4, tagNames: ["framebag", "bikepacking"] },
{
globalItemIndex: 5,
tagNames: ["top-tube-bag", "ultralight", "bikepacking"],
},
{
globalItemIndex: 6,
tagNames: ["tent", "ultralight", "premium", "hiking"],
},
{ globalItemIndex: 7, tagNames: ["tent", "premium", "hiking"] },
{ globalItemIndex: 8, tagNames: ["tent", "ultralight", "budget", "hiking"] },
{ globalItemIndex: 9, tagNames: ["tent", "ultralight", "hiking", "camping"] },
{
globalItemIndex: 10,
tagNames: ["quilt", "ultralight", "premium", "hiking"],
},
{
globalItemIndex: 11,
tagNames: ["sleeping-pad", "ultralight", "premium", "hiking"],
},
{
globalItemIndex: 12,
tagNames: ["sleeping-pad", "hiking", "camping"],
},
{ globalItemIndex: 13, tagNames: ["pillow", "ultralight", "hiking"] },
{
globalItemIndex: 14,
tagNames: ["sleeping-bag", "premium", "hiking", "camping"],
},
{
globalItemIndex: 15,
tagNames: ["stove", "ultralight", "budget", "bikepacking"],
},
{ globalItemIndex: 16, tagNames: ["stove", "premium", "bikepacking"] },
{
globalItemIndex: 17,
tagNames: ["cookware", "ultralight", "bikepacking"],
},
{ globalItemIndex: 18, tagNames: ["cookware", "camping"] },
{ globalItemIndex: 19, tagNames: ["stove", "camping", "hiking"] },
{
globalItemIndex: 20,
tagNames: ["headlamp", "ultralight", "bikepacking", "hiking"],
},
{ globalItemIndex: 21, tagNames: ["bike-light", "bikepacking"] },
{ globalItemIndex: 22, tagNames: ["headlamp", "camping", "hiking"] },
{ globalItemIndex: 29, tagNames: ["water-filter", "ultralight", "hiking"] },
{ globalItemIndex: 30, tagNames: ["water-filter", "ultralight", "hiking"] },
{
globalItemIndex: 31,
tagNames: ["water-bottle", "ultralight", "bikepacking"],
},
{ globalItemIndex: 32, tagNames: ["bikepacking", "touring"] },
{ globalItemIndex: 33, tagNames: ["premium", "hiking", "bikepacking"] },
{ globalItemIndex: 34, tagNames: ["bikepacking", "premium"] },
{ globalItemIndex: 35, tagNames: ["handlebar-bag", "waterproof", "touring"] },
] as const;
// ── Category name mapping (for FK lookups by category name) ────────
const CATEGORY_MAP: Record<string, string> = {
bags: "Bags",
shelter: "Shelter",
sleep: "Sleep System",
cooking: "Cooking",
lighting: "Lighting",
tools: "Tools & Repair",
clothing: "Clothing",
water: "Water",
electronics: "Electronics",
navigation: "Navigation",
};
export function categoryDisplayName(seedCategory: string): string {
return CATEGORY_MAP[seedCategory] ?? seedCategory;
}
// ── User Items ─────────────────────────────────────────────────────
// globalItemIndex: index into DEV_GLOBAL_ITEMS (null = standalone item)
// categoryName: matches DEV_CATEGORIES name for FK lookup
export const DEV_USER_ITEMS = [
// Catalog-linked items (10)
{
globalItemIndex: 0 as number | null,
categoryName: "Bags",
name: "Revelate Designs Terrapin System",
weightGrams: 529,
priceCents: 18500,
purchasePriceCents: 16200,
notes: "Bought used on r/BikeparkingMarket, great condition.",
quantity: 1,
},
{
globalItemIndex: 2,
categoryName: "Bags",
name: "Ortlieb Frame-Pack RC",
weightGrams: 250,
priceCents: 12000,
purchasePriceCents: 12000,
notes: null,
quantity: 1,
},
{
globalItemIndex: 5,
categoryName: "Bags",
name: "Apidura Racing Top Tube Pack",
weightGrams: 72,
priceCents: 5500,
purchasePriceCents: 4900,
notes: null,
quantity: 1,
},
{
globalItemIndex: 8,
categoryName: "Shelter",
name: "Durston X-Mid 1 Solid",
weightGrams: 880,
priceCents: 22000,
purchasePriceCents: 22000,
notes: "Drop 2 batch, arrived March 2025.",
quantity: 1,
},
{
globalItemIndex: 10,
categoryName: "Sleep System",
name: "Enlightened Equipment Enigma 20F",
weightGrams: 567,
priceCents: 35500,
purchasePriceCents: 31000,
notes: "Custom order: 20F, 850FP, regular/regular, teal/charcoal.",
quantity: 1,
},
{
globalItemIndex: 11,
categoryName: "Sleep System",
name: "Therm-a-Rest NeoAir XLite NXT",
weightGrams: 354,
priceCents: 21000,
purchasePriceCents: null,
notes: null,
quantity: 1,
},
{
globalItemIndex: 15,
categoryName: "Cooking",
name: "BRS BRS-3000T",
weightGrams: 25,
priceCents: 2000,
purchasePriceCents: 1800,
notes: null,
quantity: 1,
},
{
globalItemIndex: 20,
categoryName: "Lighting",
name: "Nitecore NU25 UL",
weightGrams: 28,
priceCents: 3600,
purchasePriceCents: 3600,
notes: "Swapped stock strap for Litesmith strap (-5g).",
quantity: 1,
},
{
globalItemIndex: 29,
categoryName: "Water",
name: "Sawyer Squeeze SP129",
weightGrams: 85,
priceCents: 3500,
purchasePriceCents: 3500,
notes: null,
quantity: 1,
},
{
globalItemIndex: 34,
categoryName: "Navigation",
name: "Wahoo ELEMNT BOLT V2",
weightGrams: 68,
priceCents: 27999,
purchasePriceCents: 23999,
notes: "Refurbished from Wahoo directly.",
quantity: 1,
},
// Standalone items (7) - no global item link
{
globalItemIndex: null,
categoryName: "Bags",
name: "Custom Frame Bag (Wanderlust Gear Co)",
weightGrams: 145,
priceCents: 8500,
purchasePriceCents: 8500,
notes: "Custom-fitted to Surly Bridge Club 54cm frame.",
quantity: 1,
},
{
globalItemIndex: null,
categoryName: "Cooking",
name: "Folding Titanium Spork",
weightGrams: 18,
priceCents: 1200,
purchasePriceCents: null,
notes: null,
quantity: 2,
},
{
globalItemIndex: null,
categoryName: "Tools & Repair",
name: "Spare Inner Tubes 700x28-35",
weightGrams: 125,
priceCents: 800,
purchasePriceCents: null,
notes: "Schwalbe SV17, presta valve.",
quantity: 2,
},
{
globalItemIndex: null,
categoryName: "Clothing",
name: "Pearl Izumi AmFIB Lobster Gloves",
weightGrams: 180,
priceCents: 6000,
purchasePriceCents: 4500,
notes: "Essential for shoulder-season rides below 5C.",
quantity: 1,
},
{
globalItemIndex: null,
categoryName: "Electronics",
name: "USB-C to Micro-USB Adapter",
weightGrams: 3,
priceCents: 500,
purchasePriceCents: null,
notes: "For charging Garmin inReach Mini 2.",
quantity: 2,
},
{
globalItemIndex: null,
categoryName: "Sleep System",
name: "Sea to Summit Aeros UL Pillow",
weightGrams: 60,
priceCents: 4000,
purchasePriceCents: 3800,
notes: null,
quantity: 1,
},
{
globalItemIndex: null,
categoryName: "Water",
name: "Nalgene Wide Mouth 1L",
weightGrams: 179,
priceCents: 1200,
purchasePriceCents: null,
notes: "Backup water carry, also doubles as hot water bottle in sleep system.",
quantity: 1,
},
] as const;
// ── Threads ────────────────────────────────────────────────────────
export const DEV_THREADS = [
{
name: "Handlebar Bag Upgrade",
status: "active",
categoryName: "Bags",
candidates: [
{
name: "Apidura Expedition Handlebar Pack",
globalItemIndex: 1 as number | null,
weightGrams: 300,
priceCents: 16000,
status: "shortlisted",
pros: "Proven waterproof design, 14L capacity, integrated dry bag",
cons: "Heavier than competitors, price premium",
notes: "Widely used in ultra-endurance racing.",
sortOrder: 1,
},
{
name: "Rockgeist BarJam",
globalItemIndex: 3,
weightGrams: 142,
priceCents: 9900,
status: "researching",
pros: "Ultra-light, modular, supports various dry bags",
cons: "Requires separate dry bag, less total protection",
notes: "Need to factor in dry bag weight.",
sortOrder: 2,
},
{
name: "Oveja Negra Bootlegger 2.0",
globalItemIndex: null,
weightGrams: 200,
priceCents: 11500,
status: "researching",
pros: "Good build quality, middle-weight option",
cons: "Smaller capacity than Apidura",
notes: null,
sortOrder: 3,
},
],
},
{
name: "Navigation Computer",
status: "active",
categoryName: "Navigation",
candidates: [
{
name: "Wahoo ELEMNT ROAM V2",
globalItemIndex: null,
weightGrams: 95,
priceCents: 39999,
status: "shortlisted",
pros: "Color display, long battery, excellent navigation",
cons: "Expensive, heavier than BOLT",
notes: "Considering upgrading from BOLT V2.",
sortOrder: 1,
},
{
name: "Hammerhead Karoo 3",
globalItemIndex: null,
weightGrams: 132,
priceCents: 49999,
status: "researching",
pros: "Touchscreen, Android-based, great mapping",
cons: "Heaviest option, most expensive, battery life concerns",
notes: null,
sortOrder: 2,
},
],
},
{
name: "Camp Stove",
status: "resolved",
categoryName: "Cooking",
candidates: [
{
name: "Soto WindMaster",
globalItemIndex: 16 as number | null,
weightGrams: 67,
priceCents: 7500,
status: "arrived",
pros: "Excellent wind performance, micro-regulator, reliable ignition",
cons: "Slightly heavier than BRS-3000T",
notes: "Winner! Ordered from REI.",
sortOrder: 1,
},
{
name: "MSR PocketRocket Deluxe",
globalItemIndex: 19,
weightGrams: 83,
priceCents: 6000,
status: "researching",
pros: "Pressure regulator, push-start ignition",
cons: "Heaviest option, no clear wind advantage over Soto",
notes: null,
sortOrder: 2,
},
],
// resolvedCandidateIndex: 0 — the Soto WindMaster wins
resolvedCandidateIndex: 0,
},
] as const;
// ── Setups ─────────────────────────────────────────────────────────
// itemIndices: indices into DEV_USER_ITEMS for FK lookup
export const DEV_SETUPS = [
{
name: "Weekend Overnighter",
isPublic: true,
items: [
{ userItemIndex: 0, classification: "base" }, // Terrapin saddle bag
{ userItemIndex: 3, classification: "base" }, // X-Mid 1
{ userItemIndex: 4, classification: "base" }, // EE Enigma
{ userItemIndex: 5, classification: "base" }, // NeoAir XLite
{ userItemIndex: 6, classification: "consumable" }, // BRS stove
{ userItemIndex: 7, classification: "worn" }, // Nitecore NU25
{ userItemIndex: 8, classification: "consumable" }, // Sawyer Squeeze
{ userItemIndex: 9, classification: "base" }, // Wahoo BOLT
],
},
{
name: "Ultra-Light Day Ride",
isPublic: false,
items: [
{ userItemIndex: 2, classification: "base" }, // Top tube pack
{ userItemIndex: 7, classification: "worn" }, // Nitecore NU25
{ userItemIndex: 9, classification: "base" }, // Wahoo BOLT
{ userItemIndex: 16, classification: "consumable" }, // Nalgene
],
},
] as const;
// ── Settings ───────────────────────────────────────────────────────
export const DEV_SETTINGS = [
{ key: "weightUnit", value: "g" },
{ key: "currency", value: "EUR" },
] as const;

303
src/db/dev-seed.ts Normal file
View File

@@ -0,0 +1,303 @@
// ── Dev Seed Runner ────────────────────────────────────────────────
// Idempotent script to populate a dev database with realistic data.
// Usage: bun run db:seed:dev
import { and, eq } from "drizzle-orm";
import {
DEV_CATEGORIES,
DEV_GLOBAL_ITEMS,
DEV_SETUPS,
DEV_SETTINGS,
DEV_TAG_ASSIGNMENTS,
DEV_THREADS,
DEV_USER_ITEMS,
categoryDisplayName,
} from "./dev-seed-data.ts";
import { db } from "./index.ts";
import * as schema from "./schema.ts";
import { seedGlobalItems } from "./seed-global-items.ts";
type Db = typeof db;
async function seedDevData(database: Db = db) {
// ── Idempotency check ──────────────────────────────────────────
const existing = await database
.select()
.from(schema.users)
.where(eq(schema.users.logtoSub, "dev-user-seed"))
.limit(1);
if (existing.length > 0) {
console.log("Dev seed data already exists, skipping.");
return;
}
try {
// ── 1. Seed global items and tags ──────────────────────────
await seedGlobalItems(database);
console.log(" Global items and tags seeded.");
// ── 2. Insert dev user ─────────────────────────────────────
const [user] = await database
.insert(schema.users)
.values({
logtoSub: "dev-user-seed",
displayName: "Dev User",
bio: "Bikepacking enthusiast and gear nerd. Always optimizing the kit.",
})
.returning();
if (!user) throw new Error("Failed to insert dev user");
const userId = user.id;
console.log(` Dev user created (id=${userId}).`);
// ── 3. Insert categories ───────────────────────────────────
const insertedCategories = await database
.insert(schema.categories)
.values(
DEV_CATEGORIES.map((c) => ({
name: c.name,
icon: c.icon,
userId,
})),
)
.returning();
const categoryByName = new Map<string, number>();
for (const cat of insertedCategories) {
categoryByName.set(cat.name, cat.id);
}
console.log(` ${insertedCategories.length} categories created.`);
// ── 4. Look up tag IDs ─────────────────────────────────────
const allTags = await database.select().from(schema.tags);
const tagNameToId = new Map<string, number>();
for (const tag of allTags) {
tagNameToId.set(tag.name, tag.id);
}
// ── 5. Insert global items and tag assignments ─────────────
// DEV_GLOBAL_ITEMS may overlap with seed-global-items.json entries.
// Insert only items that don't already exist (by brand+model).
const existingGlobalItems = await database
.select()
.from(schema.globalItems);
const existingGlobalItemMap = new Map<string, number>();
for (const gi of existingGlobalItems) {
existingGlobalItemMap.set(`${gi.brand}::${gi.model}`, gi.id);
}
const globalItemIds: number[] = [];
let newGlobalCount = 0;
for (const item of DEV_GLOBAL_ITEMS) {
const key = `${item.brand}::${item.model}`;
const existingId = existingGlobalItemMap.get(key);
if (existingId) {
globalItemIds.push(existingId);
} else {
const [inserted] = await database
.insert(schema.globalItems)
.values({
brand: item.brand,
model: item.model,
category: item.category,
weightGrams: item.weightGrams,
priceCents: item.priceCents,
description: item.description,
})
.returning();
if (!inserted) throw new Error(`Failed to insert global item: ${item.brand} ${item.model}`);
globalItemIds.push(inserted.id);
newGlobalCount++;
}
}
console.log(
` ${globalItemIds.length} global items mapped (${newGlobalCount} new).`,
);
// Insert tag assignments
let tagAssignmentCount = 0;
for (const assignment of DEV_TAG_ASSIGNMENTS) {
const giId = globalItemIds[assignment.globalItemIndex];
if (!giId) continue;
for (const tagName of assignment.tagNames) {
const tagId = tagNameToId.get(tagName);
if (!tagId) continue;
// Skip if already exists
const existingLink = await database
.select()
.from(schema.globalItemTags)
.where(
and(
eq(schema.globalItemTags.globalItemId, giId),
eq(schema.globalItemTags.tagId, tagId),
),
)
.limit(1);
if (existingLink.length === 0) {
await database
.insert(schema.globalItemTags)
.values({ globalItemId: giId, tagId });
tagAssignmentCount++;
}
}
}
console.log(` ${tagAssignmentCount} tag assignments created.`);
// ── 6. Insert user items ───────────────────────────────────
const userItemValues = DEV_USER_ITEMS.map((item) => ({
name: item.name,
weightGrams: item.weightGrams,
priceCents: item.priceCents,
categoryId: categoryByName.get(item.categoryName)!,
userId,
notes: item.notes,
quantity: item.quantity,
globalItemId:
item.globalItemIndex !== null
? globalItemIds[item.globalItemIndex]
: null,
purchasePriceCents: item.purchasePriceCents,
}));
const insertedItems = await database
.insert(schema.items)
.values(userItemValues)
.returning();
console.log(` ${insertedItems.length} user items created.`);
// ── 7. Insert threads ──────────────────────────────────────
const threadResults: Array<{
threadId: number;
threadDef: (typeof DEV_THREADS)[number];
}> = [];
for (const threadDef of DEV_THREADS) {
const catId = categoryByName.get(threadDef.categoryName);
if (!catId) continue;
const [thread] = await database
.insert(schema.threads)
.values({
name: threadDef.name,
status: threadDef.status,
categoryId: catId,
userId,
})
.returning();
if (!thread) throw new Error(`Failed to insert thread: ${threadDef.name}`);
threadResults.push({ threadId: thread.id, threadDef });
}
console.log(` ${threadResults.length} threads created.`);
// ── 8. Insert thread candidates ────────────────────────────
let candidateCount = 0;
for (const { threadId, threadDef } of threadResults) {
const catId = categoryByName.get(threadDef.categoryName)!;
const insertedCandidates = [];
for (const cand of threadDef.candidates) {
const [inserted] = await database
.insert(schema.threadCandidates)
.values({
threadId,
name: cand.name,
weightGrams: cand.weightGrams,
priceCents: cand.priceCents,
categoryId: catId,
status: cand.status,
pros: cand.pros,
cons: cand.cons,
notes: cand.notes,
sortOrder: cand.sortOrder,
globalItemId:
cand.globalItemIndex !== null
? globalItemIds[cand.globalItemIndex]
: null,
})
.returning();
insertedCandidates.push(inserted);
candidateCount++;
}
// Set resolvedCandidateId for resolved threads
if (
"resolvedCandidateIndex" in threadDef &&
threadDef.resolvedCandidateIndex !== undefined
) {
const winnerCandidate =
insertedCandidates[threadDef.resolvedCandidateIndex];
if (winnerCandidate) {
await database
.update(schema.threads)
.set({ resolvedCandidateId: winnerCandidate.id })
.where(eq(schema.threads.id, threadId));
}
}
}
console.log(` ${candidateCount} thread candidates created.`);
// ── 9. Insert setups ───────────────────────────────────────
const setupResults: Array<{
setupId: number;
setupDef: (typeof DEV_SETUPS)[number];
}> = [];
for (const setupDef of DEV_SETUPS) {
const [setup] = await database
.insert(schema.setups)
.values({
name: setupDef.name,
userId,
isPublic: setupDef.isPublic,
})
.returning();
if (!setup) throw new Error(`Failed to insert setup: ${setupDef.name}`);
setupResults.push({ setupId: setup.id, setupDef });
}
console.log(` ${setupResults.length} setups created.`);
// ── 10. Insert setup items ─────────────────────────────────
let setupItemCount = 0;
for (const { setupId, setupDef } of setupResults) {
for (const si of setupDef.items) {
const userItem = insertedItems[si.userItemIndex];
if (!userItem) continue;
await database.insert(schema.setupItems).values({
setupId,
itemId: userItem.id,
classification: si.classification,
});
setupItemCount++;
}
}
console.log(` ${setupItemCount} setup items created.`);
// ── 11. Insert settings ────────────────────────────────────
for (const setting of DEV_SETTINGS) {
await database.insert(schema.settings).values({
userId,
key: setting.key,
value: setting.value,
});
}
console.log(` ${DEV_SETTINGS.length} settings created.`);
// ── Summary ────────────────────────────────────────────────
console.log(
`\nDev seed complete: ${globalItemIds.length} global items, ${allTags.length} tags, ${insertedItems.length} user items, ${threadResults.length} threads, ${setupResults.length} setups`,
);
} catch (err) {
console.error("Seed failed:", err);
throw err;
}
}
// ── Entry point ────────────────────────────────────────────────────
seedDevData()
.then(() => process.exit(0))
.catch((err) => {
console.error("Seed failed:", err);
process.exit(1);
});