diff --git a/drizzle/0001_rename_emoji_to_icon.sql b/drizzle/0001_rename_emoji_to_icon.sql new file mode 100644 index 0000000..7409063 --- /dev/null +++ b/drizzle/0001_rename_emoji_to_icon.sql @@ -0,0 +1,17 @@ +ALTER TABLE `categories` RENAME COLUMN `emoji` TO `icon`;--> statement-breakpoint +UPDATE `categories` SET `icon` = CASE + WHEN `icon` = '📦' THEN 'package' + WHEN `icon` = '🏕️' THEN 'tent' + WHEN `icon` = '⛺' THEN 'tent' + WHEN `icon` = '🚲' THEN 'bike' + WHEN `icon` = '📷' THEN 'camera' + WHEN `icon` = '🎒' THEN 'backpack' + WHEN `icon` = '👕' THEN 'shirt' + WHEN `icon` = '🔧' THEN 'wrench' + WHEN `icon` = '🍳' THEN 'cooking-pot' + WHEN `icon` = '🎮' THEN 'gamepad-2' + WHEN `icon` = '💻' THEN 'laptop' + WHEN `icon` = '🏔️' THEN 'mountain-snow' + WHEN `icon` = '⛰️' THEN 'mountain' + ELSE 'package' +END; diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..f70b759 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,467 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "prevId": "78e5f5c8-f8f0-43f4-93f8-5ef68154ed17", + "tables": { + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'package'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "categories_name_unique": { + "name": "categories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "items": { + "name": "items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "items_category_id_categories_id_fk": { + "name": "items_category_id_categories_id_fk", + "tableFrom": "items", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "setup_items": { + "name": "setup_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "setup_id": { + "name": "setup_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "setup_items_setup_id_setups_id_fk": { + "name": "setup_items_setup_id_setups_id_fk", + "tableFrom": "setup_items", + "tableTo": "setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "setup_items_item_id_items_id_fk": { + "name": "setup_items_item_id_items_id_fk", + "tableFrom": "setup_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "setups": { + "name": "setups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_candidates": { + "name": "thread_candidates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "thread_id": { + "name": "thread_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_candidates_thread_id_threads_id_fk": { + "name": "thread_candidates_thread_id_threads_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thread_candidates_category_id_categories_id_fk": { + "name": "thread_candidates_category_id_categories_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "threads": { + "name": "threads", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "resolved_candidate_id": { + "name": "resolved_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "threads_category_id_categories_id_fk": { + "name": "threads_category_id_categories_id_fk", + "tableFrom": "threads", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..5816e93 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1773589489626, + "tag": "0000_bitter_luckman", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1773593102000, + "tag": "0001_rename_emoji_to_icon", + "breakpoints": true + } + ] +} diff --git a/src/db/schema.ts b/src/db/schema.ts index d182609..8d2e618 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -3,7 +3,7 @@ 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}"), + icon: text("icon").notNull().default("package"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .$defaultFn(() => new Date()), diff --git a/src/db/seed.ts b/src/db/seed.ts index dfc280a..dd512fa 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -7,7 +7,7 @@ export function seedDefaults() { db.insert(categories) .values({ name: "Uncategorized", - emoji: "\u{1F4E6}", + icon: "package", }) .run(); } diff --git a/src/server/services/category.service.ts b/src/server/services/category.service.ts index 626a356..30860e9 100644 --- a/src/server/services/category.service.ts +++ b/src/server/services/category.service.ts @@ -10,13 +10,13 @@ export function getAllCategories(db: Db = prodDb) { export function createCategory( db: Db = prodDb, - data: { name: string; emoji?: string }, + data: { name: string; icon?: string }, ) { return db .insert(categories) .values({ name: data.name, - ...(data.emoji ? { emoji: data.emoji } : {}), + ...(data.icon ? { icon: data.icon } : {}), }) .returning() .get(); @@ -25,7 +25,7 @@ export function createCategory( export function updateCategory( db: Db = prodDb, id: number, - data: { name?: string; emoji?: string }, + data: { name?: string; icon?: string }, ) { const existing = db .select({ id: categories.id }) diff --git a/src/server/services/item.service.ts b/src/server/services/item.service.ts index 11b5c5e..7d484a3 100644 --- a/src/server/services/item.service.ts +++ b/src/server/services/item.service.ts @@ -19,7 +19,7 @@ export function getAllItems(db: Db = prodDb) { createdAt: items.createdAt, updatedAt: items.updatedAt, categoryName: categories.name, - categoryEmoji: categories.emoji, + categoryIcon: categories.icon, }) .from(items) .innerJoin(categories, eq(items.categoryId, categories.id)) diff --git a/src/server/services/setup.service.ts b/src/server/services/setup.service.ts index f19bdb5..4e7543a 100644 --- a/src/server/services/setup.service.ts +++ b/src/server/services/setup.service.ts @@ -57,7 +57,7 @@ export function getSetupWithItems(db: Db = prodDb, setupId: number) { createdAt: items.createdAt, updatedAt: items.updatedAt, categoryName: categories.name, - categoryEmoji: categories.emoji, + categoryIcon: categories.icon, }) .from(setupItems) .innerJoin(items, eq(setupItems.itemId, items.id)) diff --git a/src/server/services/thread.service.ts b/src/server/services/thread.service.ts index f8e82ba..c1d092b 100644 --- a/src/server/services/thread.service.ts +++ b/src/server/services/thread.service.ts @@ -22,7 +22,7 @@ export function getAllThreads(db: Db = prodDb, includeResolved = false) { resolvedCandidateId: threads.resolvedCandidateId, categoryId: threads.categoryId, categoryName: categories.name, - categoryEmoji: categories.emoji, + categoryIcon: categories.icon, createdAt: threads.createdAt, updatedAt: threads.updatedAt, candidateCount: sql`( @@ -67,7 +67,7 @@ export function getThreadWithCandidates(db: Db = prodDb, threadId: number) { createdAt: threadCandidates.createdAt, updatedAt: threadCandidates.updatedAt, categoryName: categories.name, - categoryEmoji: categories.emoji, + categoryIcon: categories.icon, }) .from(threadCandidates) .innerJoin(categories, eq(threadCandidates.categoryId, categories.id)) diff --git a/src/server/services/totals.service.ts b/src/server/services/totals.service.ts index d9c1fc4..1f8a3b7 100644 --- a/src/server/services/totals.service.ts +++ b/src/server/services/totals.service.ts @@ -9,7 +9,7 @@ export function getCategoryTotals(db: Db = prodDb) { .select({ categoryId: items.categoryId, categoryName: categories.name, - categoryEmoji: categories.emoji, + categoryIcon: categories.icon, totalWeight: sql`COALESCE(SUM(${items.weightGrams}), 0)`, totalCost: sql`COALESCE(SUM(${items.priceCents}), 0)`, itemCount: sql`COUNT(*)`, diff --git a/src/shared/schemas.ts b/src/shared/schemas.ts index 71078c0..0dd0b88 100644 --- a/src/shared/schemas.ts +++ b/src/shared/schemas.ts @@ -16,13 +16,13 @@ export const updateItemSchema = createItemSchema.partial().extend({ export const createCategorySchema = z.object({ name: z.string().min(1, "Category name is required"), - emoji: z.string().min(1).max(4).default("\u{1F4E6}"), + icon: z.string().min(1).max(50).default("package"), }); export const updateCategorySchema = z.object({ id: z.number().int().positive(), name: z.string().min(1).optional(), - emoji: z.string().min(1).max(4).optional(), + icon: z.string().min(1).max(50).optional(), }); // Thread schemas diff --git a/tests/helpers/db.ts b/tests/helpers/db.ts index 1de3956..90a688c 100644 --- a/tests/helpers/db.ts +++ b/tests/helpers/db.ts @@ -11,7 +11,7 @@ export function createTestDb() { CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, - emoji TEXT NOT NULL DEFAULT '📦', + icon TEXT NOT NULL DEFAULT 'package', created_at INTEGER NOT NULL DEFAULT (unixepoch()) ) `); @@ -87,7 +87,7 @@ export function createTestDb() { // Seed default Uncategorized category db.insert(schema.categories) - .values({ name: "Uncategorized", emoji: "\u{1F4E6}" }) + .values({ name: "Uncategorized", icon: "package" }) .run(); return db; diff --git a/tests/routes/categories.test.ts b/tests/routes/categories.test.ts index a141d84..216b9b8 100644 --- a/tests/routes/categories.test.ts +++ b/tests/routes/categories.test.ts @@ -31,13 +31,13 @@ describe("Category Routes", () => { const res = await app.request("/api/categories", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: "Shelter", emoji: "\u{26FA}" }), + body: JSON.stringify({ name: "Shelter", icon: "tent" }), }); expect(res.status).toBe(201); const body = await res.json(); expect(body.name).toBe("Shelter"); - expect(body.emoji).toBe("\u{26FA}"); + expect(body.icon).toBe("tent"); expect(body.id).toBeGreaterThan(0); }); @@ -55,7 +55,7 @@ describe("Category Routes", () => { const catRes = await app.request("/api/categories", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: "Shelter", emoji: "\u{26FA}" }), + body: JSON.stringify({ name: "Shelter", icon: "tent" }), }); const cat = await catRes.json(); diff --git a/tests/services/category.service.test.ts b/tests/services/category.service.test.ts index 806d51a..87fc435 100644 --- a/tests/services/category.service.test.ts +++ b/tests/services/category.service.test.ts @@ -18,27 +18,27 @@ describe("Category Service", () => { }); describe("createCategory", () => { - it("creates with name and emoji", () => { - const cat = createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); + it("creates with name and icon", () => { + const cat = createCategory(db, { name: "Shelter", icon: "tent" }); expect(cat).toBeDefined(); expect(cat!.id).toBeGreaterThan(0); expect(cat!.name).toBe("Shelter"); - expect(cat!.emoji).toBe("\u{26FA}"); + expect(cat!.icon).toBe("tent"); }); - it("uses default emoji if not provided", () => { + it("uses default icon if not provided", () => { const cat = createCategory(db, { name: "Cooking" }); expect(cat).toBeDefined(); - expect(cat!.emoji).toBe("\u{1F4E6}"); + expect(cat!.icon).toBe("package"); }); }); describe("getAllCategories", () => { it("returns all categories", () => { - createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); - createCategory(db, { name: "Cooking", emoji: "\u{1F373}" }); + createCategory(db, { name: "Shelter", icon: "tent" }); + createCategory(db, { name: "Cooking", icon: "cooking-pot" }); const all = getAllCategories(db); // Includes seeded Uncategorized + 2 new @@ -48,20 +48,20 @@ describe("Category Service", () => { describe("updateCategory", () => { it("renames category", () => { - const cat = createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); + const cat = createCategory(db, { name: "Shelter", icon: "tent" }); const updated = updateCategory(db, cat!.id, { name: "Sleep System" }); expect(updated).toBeDefined(); expect(updated!.name).toBe("Sleep System"); - expect(updated!.emoji).toBe("\u{26FA}"); + expect(updated!.icon).toBe("tent"); }); - it("changes emoji", () => { - const cat = createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); - const updated = updateCategory(db, cat!.id, { emoji: "\u{1F3E0}" }); + it("changes icon", () => { + const cat = createCategory(db, { name: "Shelter", icon: "tent" }); + const updated = updateCategory(db, cat!.id, { icon: "home" }); expect(updated).toBeDefined(); - expect(updated!.emoji).toBe("\u{1F3E0}"); + expect(updated!.icon).toBe("home"); }); it("returns null for non-existent id", () => { @@ -72,7 +72,7 @@ describe("Category Service", () => { describe("deleteCategory", () => { it("reassigns items to Uncategorized (id=1) then deletes", () => { - const shelter = createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); + const shelter = createCategory(db, { name: "Shelter", icon: "tent" }); createItem(db, { name: "Tent", categoryId: shelter!.id }); createItem(db, { name: "Tarp", categoryId: shelter!.id }); diff --git a/tests/services/item.service.test.ts b/tests/services/item.service.test.ts index cb569be..673a9c8 100644 --- a/tests/services/item.service.test.ts +++ b/tests/services/item.service.test.ts @@ -61,7 +61,7 @@ describe("Item Service", () => { const all = getAllItems(db); expect(all).toHaveLength(2); expect(all[0].categoryName).toBe("Uncategorized"); - expect(all[0].categoryEmoji).toBeDefined(); + expect(all[0].categoryIcon).toBeDefined(); }); }); diff --git a/tests/services/setup.service.test.ts b/tests/services/setup.service.test.ts index 2a9b6f2..d58d3df 100644 --- a/tests/services/setup.service.test.ts +++ b/tests/services/setup.service.test.ts @@ -83,7 +83,7 @@ describe("Setup Service", () => { expect(result!.items).toHaveLength(1); expect(result!.items[0].name).toBe("Water Bottle"); expect(result!.items[0].categoryName).toBe("Uncategorized"); - expect(result!.items[0].categoryEmoji).toBeDefined(); + expect(result!.items[0].categoryIcon).toBeDefined(); }); it("returns null for non-existent setup", () => { diff --git a/tests/services/thread.service.test.ts b/tests/services/thread.service.test.ts index b92a666..7c3eaa1 100644 --- a/tests/services/thread.service.test.ts +++ b/tests/services/thread.service.test.ts @@ -100,7 +100,7 @@ describe("Thread Service", () => { expect(result!.candidates).toHaveLength(1); expect(result!.candidates[0].name).toBe("Tent A"); expect(result!.candidates[0].categoryName).toBe("Uncategorized"); - expect(result!.candidates[0].categoryEmoji).toBeDefined(); + expect(result!.candidates[0].categoryIcon).toBeDefined(); }); it("returns null for non-existent thread", () => { diff --git a/tests/services/totals.test.ts b/tests/services/totals.test.ts index 09d2f9b..947a441 100644 --- a/tests/services/totals.test.ts +++ b/tests/services/totals.test.ts @@ -16,7 +16,7 @@ describe("Totals Service", () => { describe("getCategoryTotals", () => { it("returns weight sum, cost sum, item count per category", () => { - const shelter = createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); + const shelter = createCategory(db, { name: "Shelter", icon: "tent" }); createItem(db, { name: "Tent", weightGrams: 1200, @@ -39,7 +39,7 @@ describe("Totals Service", () => { }); it("excludes empty categories (no items)", () => { - createCategory(db, { name: "Shelter", emoji: "\u{26FA}" }); + createCategory(db, { name: "Shelter", icon: "tent" }); // No items added const totals = getCategoryTotals(db); expect(totals).toHaveLength(0);