feat(01-01): add database schema, shared Zod schemas, seed, and test infrastructure

- Create Drizzle schema with items, categories, and settings tables
- Set up database connection singleton with WAL mode and foreign keys
- Add seed script for default Uncategorized category
- Create shared Zod validation schemas for items and categories
- Export TypeScript types inferred from Zod and Drizzle schemas
- Add in-memory SQLite test helper for isolated test databases
- Wire seed into Hono server startup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 22:34:53 +01:00
parent 67ff86039f
commit 7412ef1d86
9 changed files with 233 additions and 0 deletions

9
src/db/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from "./schema.ts";
const sqlite = new Database("gearbox.db");
sqlite.run("PRAGMA journal_mode = WAL");
sqlite.run("PRAGMA foreign_keys = ON");
export const db = drizzle(sqlite, { schema });

34
src/db/schema.ts Normal file
View File

@@ -0,0 +1,34 @@
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
export const categories = sqliteTable("categories", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull().unique(),
emoji: text("emoji").notNull().default("\u{1F4E6}"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
export const items = sqliteTable("items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
weightGrams: real("weight_grams"),
priceCents: integer("price_cents"),
categoryId: integer("category_id")
.notNull()
.references(() => categories.id),
notes: text("notes"),
productUrl: text("product_url"),
imageFilename: text("image_filename"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
});
export const settings = sqliteTable("settings", {
key: text("key").primaryKey(),
value: text("value").notNull(),
});

14
src/db/seed.ts Normal file
View File

@@ -0,0 +1,14 @@
import { db } from "./index.ts";
import { categories } from "./schema.ts";
export function seedDefaults() {
const existing = db.select().from(categories).all();
if (existing.length === 0) {
db.insert(categories)
.values({
name: "Uncategorized",
emoji: "\u{1F4E6}",
})
.run();
}
}

View File

@@ -1,5 +1,9 @@
import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { seedDefaults } from "../db/seed.ts";
// Seed default data on startup
seedDefaults();
const app = new Hono();

25
src/shared/schemas.ts Normal file
View File

@@ -0,0 +1,25 @@
import { z } from "zod";
export const createItemSchema = z.object({
name: z.string().min(1, "Name is required"),
weightGrams: z.number().nonnegative().optional(),
priceCents: z.number().int().nonnegative().optional(),
categoryId: z.number().int().positive(),
notes: z.string().optional(),
productUrl: z.string().url().optional().or(z.literal("")),
});
export const updateItemSchema = createItemSchema.partial().extend({
id: z.number().int().positive(),
});
export const createCategorySchema = z.object({
name: z.string().min(1, "Category name is required"),
emoji: z.string().min(1).max(4).default("\u{1F4E6}"),
});
export const updateCategorySchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1).optional(),
emoji: z.string().min(1).max(4).optional(),
});

18
src/shared/types.ts Normal file
View File

@@ -0,0 +1,18 @@
import type { z } from "zod";
import type {
createItemSchema,
updateItemSchema,
createCategorySchema,
updateCategorySchema,
} from "./schemas.ts";
import type { items, categories } from "../db/schema.ts";
// Types inferred from Zod schemas
export type CreateItem = z.infer<typeof createItemSchema>;
export type UpdateItem = z.infer<typeof updateItemSchema>;
export type CreateCategory = z.infer<typeof createCategorySchema>;
export type UpdateCategory = z.infer<typeof updateCategorySchema>;
// Types inferred from Drizzle schema
export type Item = typeof items.$inferSelect;
export type Category = typeof categories.$inferSelect;