feat(19-01): update Zod schemas, types, and seed script for reference model

- Add globalItemId and purchasePriceCents to createItemSchema
- Add globalItemId to createCandidateSchema
- Add tags param to searchGlobalItemsSchema
- Remove linkItemSchema from schemas and types
- Replace ItemGlobalLink with Tag and GlobalItemTag types
- Convert seedGlobalItems to async, add seedTags with 30 curated tags
This commit is contained in:
2026-04-05 20:27:51 +02:00
parent 5df513c138
commit e9baa8d7e0
3 changed files with 67 additions and 21 deletions

View File

@@ -1,27 +1,73 @@
import seedData from "./global-items-seed.json"; import seedData from "./global-items-seed.json";
import { db as prodDb } from "./index.ts"; import { db as prodDb } from "./index.ts";
import { globalItems } from "./schema.ts"; import { globalItems, tags } from "./schema.ts";
type Db = typeof prodDb; type Db = typeof prodDb;
const SEED_TAGS = [
"handlebar-bag",
"framebag",
"saddlebag",
"top-tube-bag",
"stem-bag",
"fork-bag",
"hip-pack",
"backpack",
"tent",
"bivy",
"tarp",
"hammock",
"sleeping-bag",
"sleeping-pad",
"quilt",
"pillow",
"stove",
"cookware",
"water-filter",
"water-bottle",
"headlamp",
"bike-light",
"ultralight",
"waterproof",
"budget",
"premium",
"bikepacking",
"hiking",
"camping",
"touring",
];
/**
* Seed curated tags for outdoor/adventure gear.
* Idempotent: skips if any tags already exist.
*/
export async function seedTags(db: Db = prodDb) {
const existing = await db.select().from(tags).limit(1);
if (existing.length > 0) return;
for (const name of SEED_TAGS) {
await db.insert(tags).values({ name });
}
}
/** /**
* Seed the global items table with initial bikepacking gear data. * Seed the global items table with initial bikepacking gear data.
* Idempotent: skips if any rows already exist. * Idempotent: skips if any rows already exist.
*/ */
export function seedGlobalItems(db: Db = prodDb) { export async function seedGlobalItems(db: Db = prodDb) {
const existing = db.select().from(globalItems).limit(1).all(); const existing = await db.select().from(globalItems).limit(1);
if (existing.length > 0) return; if (existing.length > 0) return;
for (const item of seedData) { for (const item of seedData) {
db.insert(globalItems) await db.insert(globalItems).values({
.values({
brand: item.brand, brand: item.brand,
model: item.model, model: item.model,
category: item.category ?? null, category: item.category ?? null,
weightGrams: item.weightGrams ?? null, weightGrams: item.weightGrams ?? null,
priceCents: item.priceCents ?? null, priceCents: item.priceCents ?? null,
description: item.description ?? null, description: item.description ?? null,
}) });
.run();
} }
await seedTags(db);
} }

View File

@@ -10,6 +10,8 @@ export const createItemSchema = z.object({
imageFilename: z.string().optional(), imageFilename: z.string().optional(),
imageSourceUrl: z.string().url().optional().or(z.literal("")), imageSourceUrl: z.string().url().optional().or(z.literal("")),
quantity: z.number().int().positive().optional(), quantity: z.number().int().positive().optional(),
globalItemId: z.number().int().positive().optional(),
purchasePriceCents: z.number().int().nonnegative().optional(),
}); });
export const updateItemSchema = createItemSchema.partial().extend({ export const updateItemSchema = createItemSchema.partial().extend({
@@ -58,6 +60,7 @@ export const createCandidateSchema = z.object({
status: candidateStatusSchema.optional(), status: candidateStatusSchema.optional(),
pros: z.string().optional(), pros: z.string().optional(),
cons: z.string().optional(), cons: z.string().optional(),
globalItemId: z.number().int().positive().optional(),
}); });
export const updateCandidateSchema = createCandidateSchema.partial(); export const updateCandidateSchema = createCandidateSchema.partial();
@@ -95,10 +98,7 @@ export const updateClassificationSchema = z.object({
// Global item schemas // Global item schemas
export const searchGlobalItemsSchema = z.object({ export const searchGlobalItemsSchema = z.object({
q: z.string().optional(), q: z.string().optional(),
}); tags: z.string().optional(),
export const linkItemSchema = z.object({
globalItemId: z.number().int().positive(),
}); });
// Profile schemas // Profile schemas

View File

@@ -2,10 +2,11 @@ import type { z } from "zod";
import type { import type {
categories, categories,
globalItems, globalItems,
itemGlobalLinks, globalItemTags,
items, items,
setupItems, setupItems,
setups, setups,
tags,
threadCandidates, threadCandidates,
threads, threads,
} from "../db/schema.ts"; } from "../db/schema.ts";
@@ -15,7 +16,6 @@ import type {
createItemSchema, createItemSchema,
createSetupSchema, createSetupSchema,
createThreadSchema, createThreadSchema,
linkItemSchema,
reorderCandidatesSchema, reorderCandidatesSchema,
resolveThreadSchema, resolveThreadSchema,
searchGlobalItemsSchema, searchGlobalItemsSchema,
@@ -49,7 +49,6 @@ export type UpdateClassification = z.infer<typeof updateClassificationSchema>;
// Global item types // Global item types
export type SearchGlobalItems = z.infer<typeof searchGlobalItemsSchema>; export type SearchGlobalItems = z.infer<typeof searchGlobalItemsSchema>;
export type LinkItem = z.infer<typeof linkItemSchema>;
export type UpdateProfile = z.infer<typeof updateProfileSchema>; export type UpdateProfile = z.infer<typeof updateProfileSchema>;
// Types inferred from Drizzle schema // Types inferred from Drizzle schema
@@ -60,4 +59,5 @@ export type ThreadCandidate = typeof threadCandidates.$inferSelect;
export type Setup = typeof setups.$inferSelect; export type Setup = typeof setups.$inferSelect;
export type SetupItem = typeof setupItems.$inferSelect; export type SetupItem = typeof setupItems.$inferSelect;
export type GlobalItem = typeof globalItems.$inferSelect; export type GlobalItem = typeof globalItems.$inferSelect;
export type ItemGlobalLink = typeof itemGlobalLinks.$inferSelect; export type Tag = typeof tags.$inferSelect;
export type GlobalItemTag = typeof globalItemTags.$inferSelect;