Compare commits
5 Commits
e2dd0dc38d
...
2d71ce15af
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d71ce15af | |||
| 4b8dec6252 | |||
| 6836790e55 | |||
| eb7f37fe28 | |||
| 24f3a8a8a2 |
@@ -58,6 +58,14 @@ Requirements for this milestone. Each maps to roadmap phases.
|
|||||||
- [ ] **CATFLOW-07**: Manual entry fallback when item not in catalog
|
- [ ] **CATFLOW-07**: Manual entry fallback when item not in catalog
|
||||||
- [ ] **CATFLOW-08**: Non-functional "Submit to catalog?" prompt shown after manual save
|
- [ ] **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
|
### Tags
|
||||||
|
|
||||||
- [x] **TAG-01**: Tags table seeded with curated tag set for outdoor/adventure gear
|
- [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-01 | Phase 20 | Complete |
|
||||||
| CATFLOW-02 | 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-04 | Phase 19 | Complete |
|
||||||
| CATFLOW-05 | Phase 19, 21 | Pending |
|
| CATFLOW-05 | Phase 19, 22 | Pending |
|
||||||
| CATFLOW-06 | Phase 19, 21 | Pending |
|
| CATFLOW-06 | Phase 19, 22 | Pending |
|
||||||
| CATFLOW-07 | Phase 22 | Pending |
|
| CATFLOW-07 | Phase 23 | Pending |
|
||||||
| CATFLOW-08 | Phase 22 | Pending |
|
| CATFLOW-08 | Phase 23 | Pending |
|
||||||
| TAG-01 | Phase 19 | Complete |
|
| TAG-01 | Phase 19 | Complete |
|
||||||
| TAG-02 | 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:**
|
**Coverage:**
|
||||||
- v2.0 requirements: 40 total
|
- v2.0 requirements: 45 total
|
||||||
- Mapped to phases: 40
|
- Mapped to phases: 45
|
||||||
- Unmapped: 0
|
- Unmapped: 0
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- ✅ **v1.1 Fixes & Polish** — Phases 4-6 (shipped 2026-03-15)
|
- ✅ **v1.1 Fixes & Polish** — Phases 4-6 (shipped 2026-03-15)
|
||||||
- ✅ **v1.2 Collection Power-Ups** — Phases 7-9 (shipped 2026-03-16)
|
- ✅ **v1.2 Collection Power-Ups** — Phases 7-9 (shipped 2026-03-16)
|
||||||
- 🚧 **v1.3 Research & Decision Tools** — Phases 10-13 (in progress)
|
- 🚧 **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
|
## 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 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 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)
|
- [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 21: Item & Catalog Detail Pages** — Full detail pages for collection items and catalog entries, replacing slide-out panels
|
||||||
- [ ] **Phase 22: Manual Entry Fallback** — Manual add for items not in catalog, non-functional submission prompt
|
- [ ] **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
|
## Phase Details
|
||||||
|
|
||||||
@@ -206,9 +207,22 @@ Plans:
|
|||||||
- [x] 20-02-PLAN.md — FabMenu component, CatalogSearchOverlay component, root layout wiring
|
- [x] 20-02-PLAN.md — FabMenu component, CatalogSearchOverlay component, root layout wiring
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 21: Add-from-Catalog & Thread Integration
|
### Phase 21: Item & Catalog Detail Pages
|
||||||
**Goal**: Users can add catalog items to their collection and to threads directly from search
|
**Goal**: Collection items and catalog entries have full detail pages, replacing the slide-out panel pattern
|
||||||
**Depends on**: Phase 20
|
**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
|
**Requirements**: CATFLOW-03, CATFLOW-05, CATFLOW-06
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. User can add a catalog item to collection with one confirmation step (category picker + notes)
|
1. User can add a catalog item to collection with one confirmation step (category picker + notes)
|
||||||
@@ -217,9 +231,9 @@ Plans:
|
|||||||
**Plans**: TBD
|
**Plans**: TBD
|
||||||
**UI hint**: yes
|
**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
|
**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
|
**Requirements**: CATFLOW-07, CATFLOW-08
|
||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. User can fall back to manual entry from catalog search via "Add Manually" link
|
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 |
|
| 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 |
|
| 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 |
|
| 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 | - |
|
| 21. Item & Catalog Detail Pages | v2.0 | 0/? | Not started | - |
|
||||||
| 22. Manual Entry Fallback | 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 | - |
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ Key decisions made during v2.0 planning:
|
|||||||
|
|
||||||
None active.
|
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
|
### Blockers/Concerns
|
||||||
|
|
||||||
- Auth provider decision (Logto vs Authentik) must be resolved before Phase 15 planning
|
- Auth provider decision (Logto vs Authentik) must be resolved before Phase 15 planning
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"test": "bun test tests/",
|
"test": "bun test tests/",
|
||||||
"test:e2e": "bunx playwright test",
|
"test:e2e": "bunx playwright test",
|
||||||
"test:e2e:ui": "bunx playwright test --ui",
|
"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": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.7",
|
"@biomejs/biome": "^2.4.7",
|
||||||
|
|||||||
782
src/db/dev-seed-data.ts
Normal file
782
src/db/dev-seed-data.ts
Normal 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
303
src/db/dev-seed.ts
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user