Files
GearBox/src/db/dev-seed.ts
Jean-Luc Makiola 4ccbb2b070
Some checks failed
CI / ci (push) Failing after 1m44s
CI / e2e (push) Has been skipped
CI / deploy (push) Has been skipped
fix: wire catalog add buttons, fix Trans bold rendering, lint cleanup
- CatalogSearchOverlay: replace handleAddStub with real openAddToCollection/openAddToThread routing based on catalogSearchMode
- ConfirmDialog + __root.tsx: swap t() for Trans component on deleteItemMessage, deleteCandidateMessage, pickWinnerMessage — fixes <bold> rendering as literal text
- Biome format pass: fix 23 lint/format errors across scripts, services, tests
- Planning: mark all UAT and verification gaps resolved for phases 07, 11, 16, 20, 21, 22, 24, 32, 34; close debug sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 15:36:16 +02:00

413 lines
14 KiB
TypeScript

// ── 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<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 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<string, number>();
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);
});