// ── Dev Seed Runner ──────────────────────────────────────────────── // Clears dev data and re-seeds with fresh realistic data. // Usage: bun run db:seed:dev // Preserves real (non-dev) users. Safe to run repeatedly. import { and, eq, like, sql } from "drizzle-orm"; import { DEV_CATEGORIES, DEV_GLOBAL_ITEMS, DEV_MANUFACTURERS, DEV_MARKET_PRICES, DEV_SETTINGS, DEV_SETUPS, DEV_TAG_ASSIGNMENTS, DEV_THREADS, DEV_USER_ITEMS, } 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 clearDevData(database: Db) { console.log("Clearing existing dev seed data..."); // Find dev user(s) const devUsers = await database .select({ id: schema.users.id }) .from(schema.users) .where(like(schema.users.logtoSub, "dev-user%")); for (const user of devUsers) { // Delete in FK order: setup_items → setups, thread_candidates → threads, items, categories, settings, shares await database .delete(schema.setupItems) .where( sql`${schema.setupItems.setupId} IN (SELECT id FROM setups WHERE user_id = ${user.id})`, ); await database .delete(schema.shares) .where( sql`${schema.shares.setupId} IN (SELECT id FROM setups WHERE user_id = ${user.id})`, ); await database .delete(schema.setups) .where(eq(schema.setups.userId, user.id)); await database .delete(schema.threadCandidates) .where( sql`${schema.threadCandidates.threadId} IN (SELECT id FROM threads WHERE user_id = ${user.id})`, ); await database .delete(schema.threads) .where(eq(schema.threads.userId, user.id)); await database .delete(schema.communityPrices) .where(eq(schema.communityPrices.userId, user.id)); await database.delete(schema.items).where(eq(schema.items.userId, user.id)); await database .delete(schema.categories) .where(eq(schema.categories.userId, user.id)); await database .delete(schema.settings) .where(eq(schema.settings.userId, user.id)); await database.delete(schema.users).where(eq(schema.users.id, user.id)); console.log(` Cleared dev user id=${user.id}`); } // Clear market prices (these are global, not user-scoped, but seeded by dev) await database.delete(schema.marketPrices); console.log(" Cleared market prices."); // Global items and tags are shared — leave them (seedGlobalItems handles idempotency) console.log("Dev data cleared.\n"); } async function seedDevData(database: Db = db) { // ── Clear previous dev data ──────────────────────────────────── await clearDevData(database); try { // ── 1. Seed global items, tags, and dev-specific manufacturers ─ await seedGlobalItems(database); 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 .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(); 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(); 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 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.manufacturerId}::${gi.model}`, gi.id); } const globalItemIds: number[] = []; let newGlobalCount = 0; for (const item of DEV_GLOBAL_ITEMS) { 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); } else { const [inserted] = await database .insert(schema.globalItems) .values({ manufacturerId: mfId, 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.manufacturerSlug} ${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, visibility: setupDef.visibility, }) .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.`); // ── 12. Insert market prices ─────────────────────────────── let marketPriceCount = 0; for (const mp of DEV_MARKET_PRICES) { const giId = globalItemIds[mp.globalItemIndex]; if (!giId) continue; await database.insert(schema.marketPrices).values({ globalItemId: giId, market: mp.market, currency: mp.currency, priceCents: mp.priceCents, source: mp.source, }); marketPriceCount++; } console.log(` ${marketPriceCount} market prices created.`); // ── 13. Insert community prices ──────────────────────────── // Seed a few community prices from the dev user for items they own const ownedGlobalItemIds = insertedItems .filter((i) => i.globalItemId !== null) .map((i) => i.globalItemId as number); let communityPriceCount = 0; for (const giId of ownedGlobalItemIds.slice(0, 5)) { const item = insertedItems.find((i) => i.globalItemId === giId); if (!item) continue; await database.insert(schema.communityPrices).values({ globalItemId: giId, userId, market: "EU", currency: "EUR", priceCents: item.priceCents ? Math.round(item.priceCents * 0.85) : 10000, priceDate: new Date("2026-03-15"), sourceType: "purchased", }); communityPriceCount++; } console.log(` ${communityPriceCount} community prices created.`); // ── Summary ──────────────────────────────────────────────── console.log( `\nDev seed complete: ${globalItemIds.length} global items, ${allTags.length} tags, ${insertedItems.length} user items, ${threadResults.length} threads, ${setupResults.length} setups, ${marketPriceCount} market prices`, ); } 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); });