From 44b1eac0ba2fd05bd7e8065807bff35eefbfb378 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 18 Apr 2026 16:37:27 +0200 Subject: [PATCH] feat(catalog): migrate dev seed data to manufacturer-slug-based global items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace brand text field with manufacturerSlug in DEV_GLOBAL_ITEMS, global-items-seed.json, and seed-global-items.ts. Add DEV_MANUFACTURERS for dev-only brands not in SEED_MANUFACTURERS. Expand SEED_MANUFACTURERS with 8 additional manufacturers referenced by seed JSON (Nemo, Therm-a-Rest, Toaks, Katadyn, HydraPak, Nitecore, Outdoor Research, Exposure Lights). Update dev-seed.ts to resolve slug→id before insert and use manufacturerId as the deduplication key. Co-Authored-By: Claude Sonnet 4.6 --- src/db/dev-seed-data.ts | 93 +++++++++++++++++++++-------------- src/db/dev-seed.ts | 27 +++++++--- src/db/global-items-seed.json | 36 +++++++------- src/db/seed-global-items.ts | 13 ++++- 4 files changed, 106 insertions(+), 63 deletions(-) diff --git a/src/db/dev-seed-data.ts b/src/db/dev-seed-data.ts index 48f8df0..caec80b 100644 --- a/src/db/dev-seed-data.ts +++ b/src/db/dev-seed-data.ts @@ -17,13 +17,34 @@ export const DEV_CATEGORIES = [ { name: "Navigation", icon: "compass" }, ] as const; +// ── Manufacturers ────────────────────────────────────────────────── +// Seeded with onConflictDoNothing — safe to overlap with SEED_MANUFACTURERS. + +export const DEV_MANUFACTURERS = [ + { name: "Rockgeist", slug: "rockgeist", website: "https://rockgeist.com", country: "US", tier: 1 }, + { name: "Oveja Negra", slug: "oveja-negra", website: "https://ovejanegrabikewear.com", country: "US", tier: 1 }, + { name: "Durston", slug: "durston", website: "https://durstondesigns.com", country: "US", tier: 1 }, + { name: "Enlightened Equipment", slug: "enlightened-equipment", website: "https://enlightenedequipment.com", country: "US", tier: 1 }, + { name: "BRS", slug: "brs", website: "https://brs-outdoor.com", country: "CN", tier: 1 }, + { name: "Soto", slug: "soto", website: "https://sotostoves.com", country: "JP", tier: 1 }, + { name: "Snow Peak", slug: "snow-peak", website: "https://snowpeak.com", country: "JP", tier: 1 }, + { name: "Lezyne", slug: "lezyne", website: "https://lezyne.com", country: "US", tier: 1 }, + { name: "Fenix", slug: "fenix", website: "https://fenixlighting.com", country: "CN", tier: 1 }, + { name: "Park Tool", slug: "park-tool", website: "https://parktool.com", country: "US", tier: 1 }, + { name: "Gorilla Tape", slug: "gorilla-tape", website: "https://gorillatough.com", country: "US", tier: 1 }, + { name: "Patagonia", slug: "patagonia", website: "https://patagonia.com", country: "US", tier: 1 }, + { name: "Frogg Toggs", slug: "frogg-toggs", website: "https://froggtoggs.com", country: "US", tier: 1 }, + { name: "Buff", slug: "buff", website: "https://buffwear.com", country: "ES", tier: 1 }, + { name: "Anker", slug: "anker", website: "https://anker.com", country: "CN", tier: 1 }, +] 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", + manufacturerSlug: "revelate-designs", model: "Terrapin System", category: "bags", weightGrams: 529, @@ -32,7 +53,7 @@ export const DEV_GLOBAL_ITEMS = [ "Waterproof saddle bag with 14L capacity, roll-top closure, and integrated seat bag mount.", }, { - brand: "Apidura", + manufacturerSlug: "apidura", model: "Expedition Handlebar Pack", category: "bags", weightGrams: 300, @@ -41,7 +62,7 @@ export const DEV_GLOBAL_ITEMS = [ "14L waterproof handlebar roll bag with internal dry bag and accessory pocket.", }, { - brand: "Ortlieb", + manufacturerSlug: "ortlieb", model: "Frame-Pack RC", category: "bags", weightGrams: 250, @@ -50,7 +71,7 @@ export const DEV_GLOBAL_ITEMS = [ "6L waterproof roll-closure frame bag with TIZIP zipper for full-frame bikes.", }, { - brand: "Rockgeist", + manufacturerSlug: "rockgeist", model: "BarJam", category: "bags", weightGrams: 142, @@ -59,7 +80,7 @@ export const DEV_GLOBAL_ITEMS = [ "Ultralight handlebar harness with side-loading dry bag compatibility.", }, { - brand: "Oveja Negra", + manufacturerSlug: "oveja-negra", model: "Superwedgie", category: "bags", weightGrams: 170, @@ -68,7 +89,7 @@ export const DEV_GLOBAL_ITEMS = [ "Half-frame bag with easy-access zipper and internal organization.", }, { - brand: "Apidura", + manufacturerSlug: "apidura", model: "Racing Top Tube Pack", category: "bags", weightGrams: 72, @@ -79,7 +100,7 @@ export const DEV_GLOBAL_ITEMS = [ // Shelter (indices 6-9) { - brand: "Zpacks", + manufacturerSlug: "zpacks", model: "Duplex", category: "shelter", weightGrams: 539, @@ -88,7 +109,7 @@ export const DEV_GLOBAL_ITEMS = [ "Dyneema Composite Fabric two-person trekking pole shelter, freestanding with optional poles.", }, { - brand: "Tarptent", + manufacturerSlug: "tarptent", model: "Stratospire Li", category: "shelter", weightGrams: 737, @@ -97,7 +118,7 @@ export const DEV_GLOBAL_ITEMS = [ "Two-person double-wall tent in Dyneema with dual vestibules and excellent ventilation.", }, { - brand: "Durston", + manufacturerSlug: "durston", model: "X-Mid 1 Solid", category: "shelter", weightGrams: 880, @@ -106,7 +127,7 @@ export const DEV_GLOBAL_ITEMS = [ "Single-wall silpoly trekking pole tent with symmetrical design and two vestibules.", }, { - brand: "Big Agnes", + manufacturerSlug: "big-agnes", model: "Copper Spur HV UL1", category: "shelter", weightGrams: 936, @@ -117,7 +138,7 @@ export const DEV_GLOBAL_ITEMS = [ // Sleep System (indices 10-14) { - brand: "Enlightened Equipment", + manufacturerSlug: "enlightened-equipment", model: "Enigma 20F", category: "sleep", weightGrams: 567, @@ -126,7 +147,7 @@ export const DEV_GLOBAL_ITEMS = [ "20F down quilt with 850FP DownTek water-resistant fill, sewn footbox option.", }, { - brand: "Therm-a-Rest", + manufacturerSlug: "therm-a-rest", model: "NeoAir XLite NXT", category: "sleep", weightGrams: 354, @@ -135,7 +156,7 @@ export const DEV_GLOBAL_ITEMS = [ "R-value 4.5 ultralight inflatable sleeping pad with ThermaCapture reflective technology.", }, { - brand: "Nemo", + manufacturerSlug: "nemo", model: "Tensor Insulated Regular", category: "sleep", weightGrams: 425, @@ -144,7 +165,7 @@ export const DEV_GLOBAL_ITEMS = [ "R-value 4.2 insulated sleeping pad with Spaceframe baffles for stability.", }, { - brand: "Sea to Summit", + manufacturerSlug: "sea-to-summit", model: "Aeros Premium Pillow", category: "sleep", weightGrams: 79, @@ -153,7 +174,7 @@ export const DEV_GLOBAL_ITEMS = [ "Brushed 50D polyester inflatable pillow with multifunctional valve.", }, { - brand: "Western Mountaineering", + manufacturerSlug: "western-mountaineering", model: "NanoLite 22F", category: "sleep", weightGrams: 510, @@ -164,7 +185,7 @@ export const DEV_GLOBAL_ITEMS = [ // Cooking (indices 15-19) { - brand: "BRS", + manufacturerSlug: "brs", model: "BRS-3000T", category: "cooking", weightGrams: 25, @@ -173,7 +194,7 @@ export const DEV_GLOBAL_ITEMS = [ "Ultralight titanium canister stove, 25g with piezo ignition, 2700W output.", }, { - brand: "Soto", + manufacturerSlug: "soto", model: "WindMaster", category: "cooking", weightGrams: 67, @@ -182,7 +203,7 @@ export const DEV_GLOBAL_ITEMS = [ "Micro-regulator stove with concave burner head for excellent wind resistance.", }, { - brand: "Toaks", + manufacturerSlug: "toaks", model: "Light Titanium 750ml", category: "cooking", weightGrams: 86, @@ -191,7 +212,7 @@ export const DEV_GLOBAL_ITEMS = [ "Titanium pot with graduated measurements, lid, and folding bail handle.", }, { - brand: "Snow Peak", + manufacturerSlug: "snow-peak", model: "Ti-Mini Solo Combo", category: "cooking", weightGrams: 198, @@ -200,7 +221,7 @@ export const DEV_GLOBAL_ITEMS = [ "Titanium cookset with 850ml pot, lid/pan, and nesting mug for solo cooking.", }, { - brand: "MSR", + manufacturerSlug: "msr", model: "PocketRocket Deluxe", category: "cooking", weightGrams: 83, @@ -211,7 +232,7 @@ export const DEV_GLOBAL_ITEMS = [ // Lighting (indices 20-22) { - brand: "Nitecore", + manufacturerSlug: "nitecore", model: "NU25 UL", category: "lighting", weightGrams: 28, @@ -220,7 +241,7 @@ export const DEV_GLOBAL_ITEMS = [ "Rechargeable ultralight headlamp with 400 lumens, red/high-CRI aux LEDs.", }, { - brand: "Lezyne", + manufacturerSlug: "lezyne", model: "Lite Drive 1200+", category: "lighting", weightGrams: 176, @@ -229,7 +250,7 @@ export const DEV_GLOBAL_ITEMS = [ "1200 lumen USB-C rechargeable bike light with MOR optical lens design.", }, { - brand: "Fenix", + manufacturerSlug: "fenix", model: "HL60R", category: "lighting", weightGrams: 134, @@ -240,7 +261,7 @@ export const DEV_GLOBAL_ITEMS = [ // Tools & Repair (indices 23-25) { - brand: "Park Tool", + manufacturerSlug: "park-tool", model: "IB-3", category: "tools", weightGrams: 175, @@ -249,7 +270,7 @@ export const DEV_GLOBAL_ITEMS = [ "Folding hex/Torx multi-tool with 3-6mm hex, T25, Phillips and flathead.", }, { - brand: "Lezyne", + manufacturerSlug: "lezyne", model: "CNC Chain Breaker", category: "tools", weightGrams: 28, @@ -258,7 +279,7 @@ export const DEV_GLOBAL_ITEMS = [ "CNC-machined aluminum chain tool compatible with 8-12 speed chains.", }, { - brand: "Gorilla Tape", + manufacturerSlug: "gorilla-tape", model: "Mini Duct Tape Roll", category: "tools", weightGrams: 30, @@ -268,7 +289,7 @@ export const DEV_GLOBAL_ITEMS = [ // Clothing (indices 26-28) { - brand: "Patagonia", + manufacturerSlug: "patagonia", model: "R1 Air Full-Zip", category: "clothing", weightGrams: 266, @@ -277,7 +298,7 @@ export const DEV_GLOBAL_ITEMS = [ "Breathable midlayer fleece with open-knit R1 Air fabric for high-output activities.", }, { - brand: "Frogg Toggs", + manufacturerSlug: "frogg-toggs", model: "Ultra-Lite2 Rain Suit", category: "clothing", weightGrams: 340, @@ -286,7 +307,7 @@ export const DEV_GLOBAL_ITEMS = [ "Budget ultralight rain jacket and pants set, DriPore breathable material.", }, { - brand: "Buff", + manufacturerSlug: "buff", model: "Merino Wool Multifunctional", category: "clothing", weightGrams: 43, @@ -297,7 +318,7 @@ export const DEV_GLOBAL_ITEMS = [ // Water (indices 29-31) { - brand: "Sawyer", + manufacturerSlug: "sawyer", model: "Squeeze SP129", category: "water", weightGrams: 85, @@ -306,7 +327,7 @@ export const DEV_GLOBAL_ITEMS = [ "0.1 micron hollow-fiber water filter with high flow rate and backflush capability.", }, { - brand: "Katadyn", + manufacturerSlug: "katadyn", model: "BeFree 1L", category: "water", weightGrams: 63, @@ -315,7 +336,7 @@ export const DEV_GLOBAL_ITEMS = [ "EZ-Clean membrane filter with collapsible Hydrapak flask, 2L/min flow rate.", }, { - brand: "HydraPak", + manufacturerSlug: "hydrapak", model: "Seeker 2L", category: "water", weightGrams: 76, @@ -326,7 +347,7 @@ export const DEV_GLOBAL_ITEMS = [ // Electronics (indices 32-33) { - brand: "Anker", + manufacturerSlug: "anker", model: "Nano Power Bank 10000 PD", category: "electronics", weightGrams: 220, @@ -335,7 +356,7 @@ export const DEV_GLOBAL_ITEMS = [ "10000mAh 30W USB-C PD power bank with built-in display and passthrough charging.", }, { - brand: "Garmin", + manufacturerSlug: "garmin", model: "inReach Mini 2", category: "electronics", weightGrams: 100, @@ -346,7 +367,7 @@ export const DEV_GLOBAL_ITEMS = [ // Navigation (indices 34-35) { - brand: "Wahoo", + manufacturerSlug: "wahoo", model: "ELEMNT BOLT V2", category: "navigation", weightGrams: 68, @@ -355,7 +376,7 @@ export const DEV_GLOBAL_ITEMS = [ "GPS cycling computer with color display, turn-by-turn navigation, and smart trainer integration.", }, { - brand: "Ortlieb", + manufacturerSlug: "ortlieb", model: "Ultimate Six Classic", category: "navigation", weightGrams: 500, diff --git a/src/db/dev-seed.ts b/src/db/dev-seed.ts index 157de0b..2a6e98e 100644 --- a/src/db/dev-seed.ts +++ b/src/db/dev-seed.ts @@ -7,6 +7,7 @@ import { and, eq, like, sql } from "drizzle-orm"; import { DEV_CATEGORIES, DEV_GLOBAL_ITEMS, + DEV_MANUFACTURERS, DEV_MARKET_PRICES, DEV_SETTINGS, DEV_SETUPS, @@ -79,9 +80,12 @@ async function seedDevData(database: Db = db) { await clearDevData(database); try { - // ── 1. Seed global items and tags ────────────────────────── + // ── 1. Seed global items, tags, and dev-specific manufacturers ─ await seedGlobalItems(database); - console.log(" Global items and tags seeded."); + for (const m of DEV_MANUFACTURERS) { + await database.insert(schema.manufacturers).values(m).onConflictDoNothing(); + } + console.log(" Global items, tags, and manufacturers seeded."); // ── 2. Insert dev user ───────────────────────────────────── const [user] = await database @@ -123,20 +127,29 @@ async function seedDevData(database: Db = db) { // ── 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). + // Insert only items that don't already exist (by manufacturerId+model). + const allManufacturers = await database.select().from(schema.manufacturers); + const mfBySlug = new Map(allManufacturers.map((m) => [m.slug, m.id])); + const existingGlobalItems = await database .select() .from(schema.globalItems); const existingGlobalItemMap = new Map(); for (const gi of existingGlobalItems) { - existingGlobalItemMap.set(`${gi.brand}::${gi.model}`, gi.id); + existingGlobalItemMap.set(`${gi.manufacturerId}::${gi.model}`, gi.id); } const globalItemIds: number[] = []; let newGlobalCount = 0; for (const item of DEV_GLOBAL_ITEMS) { - const key = `${item.brand}::${item.model}`; + const mfId = mfBySlug.get(item.manufacturerSlug); + if (!mfId) { + console.warn(` Skipping "${item.model}" — unknown manufacturer slug: ${item.manufacturerSlug}`); + globalItemIds.push(0); // placeholder to keep index alignment + continue; + } + const key = `${mfId}::${item.model}`; const existingId = existingGlobalItemMap.get(key); if (existingId) { globalItemIds.push(existingId); @@ -144,7 +157,7 @@ async function seedDevData(database: Db = db) { const [inserted] = await database .insert(schema.globalItems) .values({ - brand: item.brand, + manufacturerId: mfId, model: item.model, category: item.category, weightGrams: item.weightGrams, @@ -154,7 +167,7 @@ async function seedDevData(database: Db = db) { .returning(); if (!inserted) throw new Error( - `Failed to insert global item: ${item.brand} ${item.model}`, + `Failed to insert global item: ${item.manufacturerSlug} ${item.model}`, ); globalItemIds.push(inserted.id); newGlobalCount++; diff --git a/src/db/global-items-seed.json b/src/db/global-items-seed.json index 2c82126..2843a6e 100644 --- a/src/db/global-items-seed.json +++ b/src/db/global-items-seed.json @@ -1,6 +1,6 @@ [ { - "brand": "Revelate Designs", + "manufacturerSlug": "revelate-designs", "model": "Terrapin System", "category": "bags", "weightGrams": 529, @@ -8,7 +8,7 @@ "description": "Waterproof saddle bag with 14L capacity, roll-top closure, and integrated Revelate seat bag mount." }, { - "brand": "Apidura", + "manufacturerSlug": "apidura", "model": "Expedition Handlebar Pack", "category": "bags", "weightGrams": 300, @@ -16,7 +16,7 @@ "description": "14L waterproof handlebar roll bag with internal dry bag and accessory pocket." }, { - "brand": "Ortlieb", + "manufacturerSlug": "ortlieb", "model": "Frame-Pack Toptube", "category": "bags", "weightGrams": 180, @@ -24,7 +24,7 @@ "description": "4L waterproof top-tube bag with magnetic closure and reflective details." }, { - "brand": "Revelate Designs", + "manufacturerSlug": "revelate-designs", "model": "Tangle Frame Bag", "category": "bags", "weightGrams": 170, @@ -32,7 +32,7 @@ "description": "Full-frame bag with water-resistant construction and multiple internal pockets." }, { - "brand": "Big Agnes", + "manufacturerSlug": "big-agnes", "model": "Copper Spur HV UL1", "category": "shelters", "weightGrams": 879, @@ -40,7 +40,7 @@ "description": "Ultralight 1-person freestanding tent with high-volume hub design and DAC Featherlite poles." }, { - "brand": "Tarptent", + "manufacturerSlug": "tarptent", "model": "Protrail Li", "category": "shelters", "weightGrams": 454, @@ -48,7 +48,7 @@ "description": "Ultralight single-wall trekking pole shelter in Dyneema composite fabric." }, { - "brand": "Outdoor Research", + "manufacturerSlug": "outdoor-research", "model": "Helium Bivy", "category": "shelters", "weightGrams": 510, @@ -56,7 +56,7 @@ "description": "Waterproof breathable bivy sack with single-hoop pole and full-zip entry." }, { - "brand": "Sea to Summit", + "manufacturerSlug": "sea-to-summit", "model": "Spark SP1", "category": "sleep-systems", "weightGrams": 375, @@ -64,7 +64,7 @@ "description": "Ultralight 850+ fill down sleeping bag rated to 40F/4C with Ultra-Dry Down." }, { - "brand": "Nemo", + "manufacturerSlug": "nemo", "model": "Tensor Ultralight Insulated Regular", "category": "sleep-systems", "weightGrams": 425, @@ -72,7 +72,7 @@ "description": "3-inch thick insulated sleeping pad with R-value 4.2 and Spaceframe baffles." }, { - "brand": "Therm-a-Rest", + "manufacturerSlug": "therm-a-rest", "model": "NeoAir XLite NXT", "category": "sleep-systems", "weightGrams": 354, @@ -80,7 +80,7 @@ "description": "Ultralight insulated air pad with R-value 4.5, Triangular Core Matrix, and WingLock valve." }, { - "brand": "MSR", + "manufacturerSlug": "msr", "model": "PocketRocket 2", "category": "cooking", "weightGrams": 73, @@ -88,7 +88,7 @@ "description": "Ultralight canister stove with adjustable flame control, boils 1L in 3.5 minutes." }, { - "brand": "Toaks", + "manufacturerSlug": "toaks", "model": "Titanium 750ml Pot", "category": "cooking", "weightGrams": 103, @@ -96,7 +96,7 @@ "description": "Ultralight titanium pot with lid and foldable handles, 750ml capacity." }, { - "brand": "Katadyn", + "manufacturerSlug": "katadyn", "model": "BeFree 1.0L", "category": "hydration", "weightGrams": 59, @@ -104,7 +104,7 @@ "description": "Ultralight hollow fiber water filter with 0.1 micron filtration and 1L soft flask." }, { - "brand": "HydraPak", + "manufacturerSlug": "hydrapak", "model": "Seeker 2L", "category": "hydration", "weightGrams": 73, @@ -112,7 +112,7 @@ "description": "Collapsible 2L water storage with wide mouth and compatible with Katadyn BeFree filter." }, { - "brand": "Nitecore", + "manufacturerSlug": "nitecore", "model": "NU25 UL", "category": "lighting", "weightGrams": 28, @@ -120,7 +120,7 @@ "description": "Ultralight USB-C rechargeable headlamp with 400 lumens max and red light mode." }, { - "brand": "Exposure Lights", + "manufacturerSlug": "exposure-lights", "model": "Revo Dynamo", "category": "lighting", "weightGrams": 130, @@ -128,7 +128,7 @@ "description": "Dynamo-powered front light with 800 lumens, built-in standlight, and USB charging output." }, { - "brand": "Surly", + "manufacturerSlug": "surly", "model": "24-Pack Rack", "category": "racks", "weightGrams": 750, @@ -136,7 +136,7 @@ "description": "Front rack for bikepacking with 24-pack platform, fits most forks with mid-blade eyelets." }, { - "brand": "Salsa", + "manufacturerSlug": "salsa-cycles", "model": "Anything Cage HD", "category": "accessories", "weightGrams": 80, diff --git a/src/db/seed-global-items.ts b/src/db/seed-global-items.ts index 6c1185c..d10dd4e 100644 --- a/src/db/seed-global-items.ts +++ b/src/db/seed-global-items.ts @@ -25,6 +25,15 @@ export const SEED_MANUFACTURERS = [ { name: "Trek", slug: "trek", website: "https://trekbikes.com", country: "US", tier: 1 }, { name: "Salsa Cycles", slug: "salsa-cycles", website: "https://salsacycles.com", country: "US", tier: 1 }, { name: "Surly", slug: "surly", website: "https://surlybikes.com", country: "US", tier: 1 }, + // Additional manufacturers referenced in seed data + { name: "Nemo", slug: "nemo", website: "https://nemoequipment.com", country: "US", tier: 1 }, + { name: "Therm-a-Rest", slug: "therm-a-rest", website: "https://thermarest.com", country: "US", tier: 1 }, + { name: "Toaks", slug: "toaks", website: "https://toaksoutdoor.com", country: "CN", tier: 1 }, + { name: "Katadyn", slug: "katadyn", website: "https://katadyn.com", country: "CH", tier: 1 }, + { name: "HydraPak", slug: "hydrapak", website: "https://hydrapak.com", country: "US", tier: 1 }, + { name: "Nitecore", slug: "nitecore", website: "https://nitecore.com", country: "CN", tier: 1 }, + { name: "Outdoor Research", slug: "outdoor-research", website: "https://outdoorresearch.com", country: "US", tier: 1 }, + { name: "Exposure Lights", slug: "exposure-lights", website: "https://exposurelights.com", country: "GB", tier: 1 }, ]; const SEED_TAGS = [ @@ -68,10 +77,10 @@ export async function seedGlobalItems(db: Db = prodDb) { if (existing.length > 0) return; const allManufacturers = await db.select().from(manufacturers); - const mfByName = new Map(allManufacturers.map((m) => [m.name, m.id])); + const mfBySlug = new Map(allManufacturers.map((m) => [m.slug, m.id])); for (const item of seedData) { - const manufacturerId = mfByName.get(item.brand); + const manufacturerId = mfBySlug.get(item.manufacturerSlug); if (!manufacturerId) continue; await db.insert(globalItems).values({