Archive roadmap, requirements, and phase directories to milestones/. Evolve PROJECT.md with validated requirements and key decisions. Reorganize ROADMAP.md with milestone grouping. Delete REQUIREMENTS.md (fresh for next milestone).
13 KiB
13 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation-and-collection | 02 | execute | 2 |
|
|
true |
|
|
Purpose: Provides the data layer and API endpoints that the frontend will consume. All four COLL requirements are addressed by the API. Output: Working Hono API routes with validated inputs, service layer, and passing test suite.
<execution_context> @/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md @/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-foundation-and-collection/01-RESEARCH.md @.planning/phases/01-foundation-and-collection/01-VALIDATION.md @.planning/phases/01-foundation-and-collection/01-01-SUMMARY.mdFrom src/db/schema.ts:
export const categories = sqliteTable("categories", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull().unique(),
emoji: text("emoji").notNull().default("..."),
createdAt: integer("created_at", { mode: "timestamp" })...
});
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" })...,
updatedAt: integer("updated_at", { mode: "timestamp" })...,
});
From src/shared/schemas.ts:
export const createItemSchema = z.object({ name, weightGrams?, priceCents?, categoryId, notes?, productUrl? });
export const updateItemSchema = createItemSchema.partial().extend({ id });
export const createCategorySchema = z.object({ name, emoji? });
export const updateCategorySchema = z.object({ id, name?, emoji? });
From tests/helpers/db.ts:
export function createTestDb(): DrizzleInstance;
Category service tests:
- createCategory: creates with name and emoji
- createCategory: uses default emoji if not provided
- getAllCategories: returns all categories
- updateCategory: renames category
- updateCategory: changes emoji
- deleteCategory: reassigns items to Uncategorized (id=1) then deletes
- deleteCategory: cannot delete Uncategorized (id=1)
Totals tests:
- getCategoryTotals: returns weight sum, cost sum, item count per category
- getCategoryTotals: excludes empty categories (no items)
- getGlobalTotals: returns overall weight, cost, count
- getGlobalTotals: returns zeros when no items exist
Write tests FIRST using createTestDb() from tests/helpers/db.ts. Each test gets a fresh in-memory DB.
Then implement services:
1. `src/server/services/item.service.ts`:
- Functions accept a db instance parameter (for testability) with default to the production db
- getAllItems(): SELECT items JOIN categories, returns items with category name and emoji
- getItemById(id): SELECT single item or null
- createItem(data: CreateItem): INSERT, return with id and timestamps
- updateItem(id, data): UPDATE with updatedAt = new Date(), return updated item
- deleteItem(id): SELECT item first (for image filename), DELETE, return the deleted item data
2. `src/server/services/category.service.ts`:
- getAllCategories(): SELECT all, ordered by name
- createCategory(data: CreateCategory): INSERT, return with id
- updateCategory(id, data): UPDATE name and/or emoji
- deleteCategory(id): Guard against deleting id=1. UPDATE all items with this categoryId to categoryId=1, then DELETE the category. Use a transaction.
3. Totals functions (can live in item.service.ts or a separate totals module):
- getCategoryTotals(): Per RESEARCH.md Pattern 4 exactly. SELECT with SUM and COUNT, GROUP BY categoryId, JOIN categories.
- getGlobalTotals(): SELECT SUM(weightGrams), SUM(priceCents), COUNT(*) from items.
bun test tests/services/ --bail
All service tests pass. Item CRUD, category CRUD with Uncategorized reassignment, and computed totals all work correctly against in-memory SQLite.
Task 2: Hono API routes with validation, image upload, and integration tests
src/server/routes/items.ts, src/server/routes/categories.ts, src/server/routes/totals.ts, src/server/routes/images.ts, src/server/index.ts, tests/routes/items.test.ts, tests/routes/categories.test.ts
1. Create `src/server/routes/items.ts` per RESEARCH.md example:
- GET / returns all items (calls getAllItems service)
- GET /:id returns single item (404 if not found)
- POST / validates body with zValidator("json", createItemSchema), calls createItem, returns 201
- PUT /:id validates body with zValidator("json", updateItemSchema), calls updateItem, returns 200 or 404
- DELETE /:id calls deleteItem, cleans up image file if item had imageFilename (try/catch, don't fail delete if file missing), returns 200 or 404
- Export as itemRoutes
2. Create `src/server/routes/categories.ts`:
- GET / returns all categories
- POST / validates with createCategorySchema, returns 201
- PUT /:id validates with updateCategorySchema, returns 200 or 404
- DELETE /:id calls deleteCategory, returns 200 or 400 (if trying to delete Uncategorized) or 404
- Export as categoryRoutes
3. Create `src/server/routes/totals.ts`:
- GET / returns { categories: CategoryTotals[], global: GlobalTotals }
- Export as totalRoutes
4. Create `src/server/routes/images.ts`:
- POST / accepts multipart/form-data with a single file field "image"
- Validate: file exists, size under 5MB, type is image/jpeg, image/png, or image/webp
- Generate unique filename: `${Date.now()}-${randomUUID()}.${extension}`
- Write to uploads/ directory using Bun.write
- Return 201 with { filename }
- Export as imageRoutes
5. Update `src/server/index.ts`:
- Register all routes: app.route("/api/items", itemRoutes), app.route("/api/categories", categoryRoutes), app.route("/api/totals", totalRoutes), app.route("/api/images", imageRoutes)
- Keep health check and static file serving from Plan 01
6. Create integration tests `tests/routes/items.test.ts`:
- Test POST /api/items with valid data returns 201
- Test POST /api/items with missing name returns 400 (Zod validation)
- Test GET /api/items returns array
- Test PUT /api/items/:id updates fields
- Test DELETE /api/items/:id returns success
7. Create integration tests `tests/routes/categories.test.ts`:
- Test POST /api/categories creates category
- Test DELETE /api/categories/:id reassigns items
- Test DELETE /api/categories/1 returns 400 (cannot delete Uncategorized)
NOTE for integration tests: Use Hono's app.request() method for testing without starting a real server. Create a test app instance with an in-memory DB injected.
bun test --bail
All API routes respond correctly. Validation rejects invalid input with 400. Item CRUD returns proper status codes. Category delete reassigns items. Totals endpoint returns computed aggregates. Image upload stores files. All integration tests pass.
- `bun test` passes all service and route tests
- `curl -X POST http://localhost:3000/api/categories -H 'Content-Type: application/json' -d '{"name":"Shelter","emoji":"tent emoji"}'` returns 201
- `curl -X POST http://localhost:3000/api/items -H 'Content-Type: application/json' -d '{"name":"Tent","categoryId":2}'` returns 201
- `curl http://localhost:3000/api/totals` returns category and global totals
- `curl -X DELETE http://localhost:3000/api/categories/1` returns 400 (cannot delete Uncategorized)
<success_criteria>
- All item CRUD operations work via API (create, read, update, delete)
- All category CRUD operations work via API including reassignment on delete
- Totals endpoint returns correct per-category and global aggregates
- Image upload endpoint accepts files and stores them in uploads/
- Zod validation rejects invalid input with 400 status
- All tests pass with bun test </success_criteria>