feat(09-01): add classification column to setupItems with service layer and tests
- Add classification text column (default 'base') to setupItems schema - Add classificationSchema and updateClassificationSchema Zod validators - Add UpdateClassification type inferred from Zod schema - Implement updateItemClassification service function - Modify getSetupWithItems to return classification field - Modify syncSetupItems to preserve classifications across re-sync - Add tests for classification CRUD, preservation, and cross-setup independence - Generate and apply Drizzle migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
drizzle/0003_misty_mongu.sql
Normal file
1
drizzle/0003_misty_mongu.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `setup_items` ADD `classification` text DEFAULT 'base' NOT NULL;
|
||||||
483
drizzle/meta/0003_snapshot.json
Normal file
483
drizzle/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "628b9ef4-c715-4bbe-a118-042d80fde91e",
|
||||||
|
"prevId": "ad8099fa-5c3f-4918-9e21-a259cae77d4f",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"classification": {
|
||||||
|
"name": "classification",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'base'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'researching'"
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1773666521689,
|
"when": 1773666521689,
|
||||||
"tag": "0002_broken_roughhouse",
|
"tag": "0002_broken_roughhouse",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1773670263013,
|
||||||
|
"tag": "0003_misty_mongu",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,7 @@ export const setupItems = sqliteTable("setup_items", {
|
|||||||
itemId: integer("item_id")
|
itemId: integer("item_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => items.id, { onDelete: "cascade" }),
|
.references(() => items.id, { onDelete: "cascade" }),
|
||||||
|
classification: text("classification").notNull().default("base"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = sqliteTable("settings", {
|
export const settings = sqliteTable("settings", {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function getSetupWithItems(db: Db = prodDb, setupId: number) {
|
|||||||
updatedAt: items.updatedAt,
|
updatedAt: items.updatedAt,
|
||||||
categoryName: categories.name,
|
categoryName: categories.name,
|
||||||
categoryIcon: categories.icon,
|
categoryIcon: categories.icon,
|
||||||
|
classification: setupItems.classification,
|
||||||
})
|
})
|
||||||
.from(setupItems)
|
.from(setupItems)
|
||||||
.innerJoin(items, eq(setupItems.itemId, items.id))
|
.innerJoin(items, eq(setupItems.itemId, items.id))
|
||||||
@@ -101,16 +102,51 @@ export function syncSetupItems(
|
|||||||
itemIds: number[],
|
itemIds: number[],
|
||||||
) {
|
) {
|
||||||
return db.transaction((tx) => {
|
return db.transaction((tx) => {
|
||||||
|
// Save existing classifications before deleting
|
||||||
|
const existing = tx
|
||||||
|
.select({
|
||||||
|
itemId: setupItems.itemId,
|
||||||
|
classification: setupItems.classification,
|
||||||
|
})
|
||||||
|
.from(setupItems)
|
||||||
|
.where(eq(setupItems.setupId, setupId))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
const classificationMap = new Map<number, string>();
|
||||||
|
for (const row of existing) {
|
||||||
|
classificationMap.set(row.itemId, row.classification);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete all existing items for this setup
|
// Delete all existing items for this setup
|
||||||
tx.delete(setupItems).where(eq(setupItems.setupId, setupId)).run();
|
tx.delete(setupItems).where(eq(setupItems.setupId, setupId)).run();
|
||||||
|
|
||||||
// Re-insert new items
|
// Re-insert new items, preserving classifications for retained items
|
||||||
for (const itemId of itemIds) {
|
for (const itemId of itemIds) {
|
||||||
tx.insert(setupItems).values({ setupId, itemId }).run();
|
tx.insert(setupItems)
|
||||||
|
.values({
|
||||||
|
setupId,
|
||||||
|
itemId,
|
||||||
|
classification: classificationMap.get(itemId) ?? "base",
|
||||||
|
})
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateItemClassification(
|
||||||
|
db: Db = prodDb,
|
||||||
|
setupId: number,
|
||||||
|
itemId: number,
|
||||||
|
classification: string,
|
||||||
|
) {
|
||||||
|
db.update(setupItems)
|
||||||
|
.set({ classification })
|
||||||
|
.where(
|
||||||
|
sql`${setupItems.setupId} = ${setupId} AND ${setupItems.itemId} = ${itemId}`,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
export function removeSetupItem(
|
export function removeSetupItem(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
setupId: number,
|
setupId: number,
|
||||||
|
|||||||
@@ -73,3 +73,10 @@ export const updateSetupSchema = z.object({
|
|||||||
export const syncSetupItemsSchema = z.object({
|
export const syncSetupItemsSchema = z.object({
|
||||||
itemIds: z.array(z.number().int().positive()),
|
itemIds: z.array(z.number().int().positive()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Classification schemas
|
||||||
|
export const classificationSchema = z.enum(["base", "worn", "consumable"]);
|
||||||
|
|
||||||
|
export const updateClassificationSchema = z.object({
|
||||||
|
classification: classificationSchema,
|
||||||
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
syncSetupItemsSchema,
|
syncSetupItemsSchema,
|
||||||
updateCandidateSchema,
|
updateCandidateSchema,
|
||||||
updateCategorySchema,
|
updateCategorySchema,
|
||||||
|
updateClassificationSchema,
|
||||||
updateItemSchema,
|
updateItemSchema,
|
||||||
updateSetupSchema,
|
updateSetupSchema,
|
||||||
updateThreadSchema,
|
updateThreadSchema,
|
||||||
@@ -37,6 +38,7 @@ export type ResolveThread = z.infer<typeof resolveThreadSchema>;
|
|||||||
export type CreateSetup = z.infer<typeof createSetupSchema>;
|
export type CreateSetup = z.infer<typeof createSetupSchema>;
|
||||||
export type UpdateSetup = z.infer<typeof updateSetupSchema>;
|
export type UpdateSetup = z.infer<typeof updateSetupSchema>;
|
||||||
export type SyncSetupItems = z.infer<typeof syncSetupItemsSchema>;
|
export type SyncSetupItems = z.infer<typeof syncSetupItemsSchema>;
|
||||||
|
export type UpdateClassification = z.infer<typeof updateClassificationSchema>;
|
||||||
|
|
||||||
// Types inferred from Drizzle schema
|
// Types inferred from Drizzle schema
|
||||||
export type Item = typeof items.$inferSelect;
|
export type Item = typeof items.$inferSelect;
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ export function createTestDb() {
|
|||||||
CREATE TABLE setup_items (
|
CREATE TABLE setup_items (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
setup_id INTEGER NOT NULL REFERENCES setups(id) ON DELETE CASCADE,
|
setup_id INTEGER NOT NULL REFERENCES setups(id) ON DELETE CASCADE,
|
||||||
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE
|
item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE,
|
||||||
|
classification TEXT NOT NULL DEFAULT 'base'
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getSetupWithItems,
|
getSetupWithItems,
|
||||||
removeSetupItem,
|
removeSetupItem,
|
||||||
syncSetupItems,
|
syncSetupItems,
|
||||||
|
updateItemClassification,
|
||||||
updateSetup,
|
updateSetup,
|
||||||
} from "../../src/server/services/setup.service.ts";
|
} from "../../src/server/services/setup.service.ts";
|
||||||
import { createTestDb } from "../helpers/db.ts";
|
import { createTestDb } from "../helpers/db.ts";
|
||||||
@@ -172,6 +173,106 @@ describe("Setup Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getSetupWithItems - classification", () => {
|
||||||
|
it("returns classification field defaulting to 'base' for each item", () => {
|
||||||
|
const setup = createSetup(db, { name: "Day Hike" });
|
||||||
|
const item = createItem(db, {
|
||||||
|
name: "Water Bottle",
|
||||||
|
categoryId: 1,
|
||||||
|
weightGrams: 200,
|
||||||
|
priceCents: 2500,
|
||||||
|
});
|
||||||
|
syncSetupItems(db, setup.id, [item.id]);
|
||||||
|
|
||||||
|
const result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items).toHaveLength(1);
|
||||||
|
expect(result?.items[0].classification).toBe("base");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("syncSetupItems - classification preservation", () => {
|
||||||
|
it("preserves existing classifications when re-syncing items", () => {
|
||||||
|
const setup = createSetup(db, { name: "Kit" });
|
||||||
|
const item1 = createItem(db, { name: "Tent", categoryId: 1 });
|
||||||
|
const item2 = createItem(db, { name: "Jacket", categoryId: 1 });
|
||||||
|
const item3 = createItem(db, { name: "Stove", categoryId: 1 });
|
||||||
|
|
||||||
|
// Initial sync
|
||||||
|
syncSetupItems(db, setup.id, [item1.id, item2.id]);
|
||||||
|
|
||||||
|
// Change classifications
|
||||||
|
updateItemClassification(db, setup.id, item1.id, "worn");
|
||||||
|
updateItemClassification(db, setup.id, item2.id, "consumable");
|
||||||
|
|
||||||
|
// Re-sync with item2 kept and item3 added (item1 removed)
|
||||||
|
syncSetupItems(db, setup.id, [item2.id, item3.id]);
|
||||||
|
|
||||||
|
const result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items).toHaveLength(2);
|
||||||
|
|
||||||
|
const item2Result = result?.items.find((i: any) => i.name === "Jacket");
|
||||||
|
const item3Result = result?.items.find((i: any) => i.name === "Stove");
|
||||||
|
expect(item2Result?.classification).toBe("consumable");
|
||||||
|
expect(item3Result?.classification).toBe("base");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assigns 'base' to newly added items with no prior classification", () => {
|
||||||
|
const setup = createSetup(db, { name: "Kit" });
|
||||||
|
const item1 = createItem(db, { name: "Item 1", categoryId: 1 });
|
||||||
|
|
||||||
|
syncSetupItems(db, setup.id, [item1.id]);
|
||||||
|
const result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items[0].classification).toBe("base");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateItemClassification", () => {
|
||||||
|
it("sets classification for a specific item in a specific setup", () => {
|
||||||
|
const setup = createSetup(db, { name: "Kit" });
|
||||||
|
const item = createItem(db, { name: "Tent", categoryId: 1 });
|
||||||
|
syncSetupItems(db, setup.id, [item.id]);
|
||||||
|
|
||||||
|
updateItemClassification(db, setup.id, item.id, "worn");
|
||||||
|
|
||||||
|
const result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items[0].classification).toBe("worn");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes item from default 'base' to 'worn'", () => {
|
||||||
|
const setup = createSetup(db, { name: "Kit" });
|
||||||
|
const item = createItem(db, { name: "Jacket", categoryId: 1 });
|
||||||
|
syncSetupItems(db, setup.id, [item.id]);
|
||||||
|
|
||||||
|
// Verify default
|
||||||
|
let result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items[0].classification).toBe("base");
|
||||||
|
|
||||||
|
// Update
|
||||||
|
updateItemClassification(db, setup.id, item.id, "worn");
|
||||||
|
|
||||||
|
result = getSetupWithItems(db, setup.id);
|
||||||
|
expect(result?.items[0].classification).toBe("worn");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("same item in two different setups can have different classifications", () => {
|
||||||
|
const setup1 = createSetup(db, { name: "Hiking" });
|
||||||
|
const setup2 = createSetup(db, { name: "Biking" });
|
||||||
|
const item = createItem(db, { name: "Jacket", categoryId: 1 });
|
||||||
|
|
||||||
|
syncSetupItems(db, setup1.id, [item.id]);
|
||||||
|
syncSetupItems(db, setup2.id, [item.id]);
|
||||||
|
|
||||||
|
updateItemClassification(db, setup1.id, item.id, "worn");
|
||||||
|
updateItemClassification(db, setup2.id, item.id, "base");
|
||||||
|
|
||||||
|
const result1 = getSetupWithItems(db, setup1.id);
|
||||||
|
const result2 = getSetupWithItems(db, setup2.id);
|
||||||
|
|
||||||
|
expect(result1?.items[0].classification).toBe("worn");
|
||||||
|
expect(result2?.items[0].classification).toBe("base");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("cascade behavior", () => {
|
describe("cascade behavior", () => {
|
||||||
it("deleting a collection item removes it from all setups", () => {
|
it("deleting a collection item removes it from all setups", () => {
|
||||||
const setup = createSetup(db, { name: "Kit" });
|
const setup = createSetup(db, { name: "Kit" });
|
||||||
|
|||||||
Reference in New Issue
Block a user