Skip to content

feat(household-items): implement CRUD API endpoints#397

Merged
steilerDev merged 3 commits into
betafrom
feat/388-household-items-crud-api
Mar 3, 2026
Merged

feat(household-items): implement CRUD API endpoints#397
steilerDev merged 3 commits into
betafrom
feat/388-household-items-crud-api

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Implement complete REST API for household items with 5 endpoints (POST, GET list, GET detail, PATCH, DELETE)
  • Service layer with pagination, filtering (category, status, vendor, room, tag), sorting, and full-text search
  • Tag replace-all semantics, document link cascade cleanup on delete
  • 90 tests (46 service + 44 route integration)

Fixes #388

Test plan

  • 46 service-level unit tests covering all CRUD operations, validation, and edge cases
  • 44 route-level integration tests via app.inject() covering auth, status codes, response shapes
  • Pre-commit hook quality gates pass (lint, typecheck, build)

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
Co-Authored-By: Claude backend-developer (Haiku) noreply@anthropic.com
Co-Authored-By: Claude qa-integration-tester (Haiku) noreply@anthropic.com

claude added 3 commits March 2, 2026 23:25
Create household items service with full CRUD operations including:
- createHouseholdItem() — create with tag linking
- getHouseholdItemById() — fetch detail with aggregated budget lines
- updateHouseholdItem() — patch with tag replace-all semantics
- deleteHouseholdItem() — delete with document link cascade cleanup
- listHouseholdItems() — paginated list with filtering and sorting

Implement 5 API endpoints:
- POST /api/household-items — 201 created
- GET /api/household-items — 200 paginated list
- GET /api/household-items/:id — 200 detail
- PATCH /api/household-items/:id — 200 updated
- DELETE /api/household-items/:id — 204 no content

Update documentLinkService to support household_item entity type with
application-layer FK validation. Register route at /api/household-items.

Fixes #388

Co-Authored-By: Claude backend-developer (Haiku 4.5) <noreply@anthropic.com>
…ory 4.2

- 46 service-level tests covering CRUD, validation, filtering, sorting, pagination
- 44 route-level integration tests via app.inject() covering all 5 endpoints
- Tests verify auth, error handling, tag replace-all, document link cascade
- Budget line aggregation (count + totalPlannedAmount) verified in list tests

Fixes #388

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude backend-developer (Haiku) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Haiku) <noreply@anthropic.com>
The household_item entity type is now supported (EPIC-04), so
document link creation for non-existent household items returns
NotFoundError (404) instead of ValidationError (400 "not yet implemented").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Haiku) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-owner] PR Review: Story 4.2 — Household Items CRUD API (#388)

Verdict: APPROVED (submitted as comment due to same-author restriction)

Acceptance Criteria Verification

AC Criterion Verdict
1 POST creates with all fields, returns 201 with populated vendor and tags PASS
2 GET list with pagination and all filters (category, status, vendorId, room, tagId, search, sortBy, sortOrder) PASS
3 GET detail with vendor (id, name), tags (id, name, color), and linked work items (id, title, status) PASS
4 PATCH updates any subset of fields; tag updates use replace-all semantics PASS
5 DELETE cascades to tag associations, work item links, and document links; returns 204 PASS
6 Validation rejects: missing name (400), invalid enums (400), invalid dates (400), non-existent vendor/tags (400) PASS
7 GET by ID returns 404 for non-existent items PASS
8 All endpoints require authentication (401 if not authenticated) PASS
9 Pagination response includes page, pageSize, totalItems, totalPages PASS
10 Search parameter performs case-insensitive matching on name and description PASS

All 10 acceptance criteria are met.

UAT Scenario Coverage

All 17 UAT scenarios (UAT-4.2-01 through UAT-4.2-17) are covered by the implementation and test suite:

  • 90 tests total (46 service-level + 44 route-level integration)
  • Tests cover create with all/minimal fields, validation errors, pagination, filtering, search, detail with associations, partial update, tag replace-all, delete cascade, and auth checks

Test Authorship

Test commit (648035a) correctly attributes Co-Authored-By: Claude qa-integration-tester (Haiku), confirming QA agent ownership of tests.

Scope Check

The PR stays within the story's scope. The only change outside the household items domain is the documentLinkService.ts update — replacing the EPIC-04 placeholder (throw new AppError(...)) with actual FK validation for household_item entity type. This is explicitly called out in the story notes and is the correct implementation approach for application-layer document link cascading.

Non-Blocking Observations

  1. Search parameter naming: AC #2 specifies search as the query parameter, but the implementation uses q. This is consistent with the existing work items API (GET /api/work-items also uses q). The codebase convention takes precedence — no change needed.

  2. Vendor summary includes specialty: AC notes say vendor should be { id, name } but HouseholdItemVendorSummary includes specialty. This is a superset of the required shape. Non-breaking, no change needed.

  3. Schema redesign impact on ACs: The original AC #1 listed planned_cost, actual_cost, and notes as flat fields. The architect redesigned these into separate household_item_budgets and household_item_notes tables (Story 4.1, PR #396). The API correctly exposes budgetLineCount and totalPlannedAmount aggregates instead. Budget line CRUD and notes CRUD will be handled in subsequent stories (4.3, 4.4). The AC should be considered met in the context of the refined architecture.

  4. Search also matches room field: AC #10 specifies search on name and description. The implementation additionally searches room. This is an enhancement that improves usability.

  5. as any type casts: Lines ~2799-2800 in the service use as any for category and status fields in toHouseholdItemSummary. This is a common pattern in Drizzle when column types don't perfectly match TypeScript enums. Flag for future refinement but not blocking.

  6. CI is still in progress: Quality Gates and Docker checks are pending. Approval is conditional on CI passing.

Decision

APPROVED — all acceptance criteria are met, UAT scenarios are addressed, test authorship is correct, and scope is clean. Conditional on CI going green.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[product-architect]

Architecture Review: Story 4.2 — Household Items CRUD API

Reviewed against wiki API-Contract.md (EPIC-04 section, lines 4755-5114), Schema.md (EPIC-04, lines 1451-1752), and Architecture.md. All changed files inspected.

Verdict: APPROVE (posted as comment due to GitHub self-review restriction)

This PR delivers a solid, well-structured CRUD API for household items that closely follows the established patterns from EPIC-03 work items. The service layer, route layer, and test coverage are all consistent with existing conventions.

What Looks Good

  1. API Contract Compliance: All 5 endpoints (POST, GET list, GET detail, PATCH, DELETE) match the API contract paths, methods, response shapes, and pagination conventions. Query parameters (page, pageSize, category, status, vendorId, room, tagId, q, sortBy, sortOrder) all match.

  2. Pattern Consistency: The service structure (separate helpers for toUserSummary, toVendorSummary, toTagResponse, validation functions, findById pattern) mirrors workItemService.ts closely. Good reuse of established conventions.

  3. Tag Replace-All Semantics: Correctly implements set-semantics for tagIds on PATCH — deletes all existing associations and re-inserts. Matches the documented PATCH convention.

  4. Document Link Cascade: The deleteHouseholdItem function correctly calls deleteLinksForEntity(db, 'household_item', id) before deleting the item itself. The documentLinkService.ts was also correctly updated to validate household_item entity existence in createLink.

  5. Pagination: Offset-based, 1-indexed pages, default 25, max 100 — matches conventions.

  6. Full-Text Search: Properly escapes LIKE wildcards (%, _) and uses parameterized queries via Drizzle sql template literals. Searches name, description, and room per the contract.

  7. JSON Schema Validation: Route-level schemas provide pre-handler validation with proper enum constraints, length limits, and additionalProperties: false. PATCH schema has minProperties: 1 to reject empty bodies.

  8. Test Coverage: 90 tests (46 service + 44 route) provide thorough coverage of happy paths, error cases, auth enforcement, and edge cases.

  9. Registration: Route registered at /api/household-items prefix in app.ts, following the established registration order.

Low Severity Issues

  1. as any casts in toHouseholdItemSummary (lines 190-191 of householdItemService.ts): item.category as any and item.status as any should not be needed. The Drizzle $inferSelect type should infer the correct string literal union from the enum definition. The work item service does NOT use as any for the same pattern. These should be item.category as HouseholdItemCategory and item.status as HouseholdItemStatus, or no cast at all if the types align. Since ESLint warns on @typescript-eslint/no-explicit-any, this likely only passes because any is a warning, not an error.

  2. API Contract Deviation — Error Code for vendor/tag not found on create/update: The API contract (line 4962) specifies HTTP 404 NOT_FOUND when a referenced vendorId or tagId does not exist on POST. The implementation throws ValidationError (HTTP 400). However, this is consistent with the work items API which also returns 400 for the same scenario (line 813 of API-Contract.md). The API contract for household items has a different specification than work items for the same scenario. This is a documentation inconsistency in the wiki, not a code bug. The implementation correctly follows the established pattern. I will update the wiki API-Contract.md to harmonize this in a future pass.

Both issues are low severity and do not block merge. The as any casts can be cleaned up in refinement.

Copy link
Copy Markdown
Owner Author

@steilerDev steilerDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security-engineer]

Security review of PR #397 — Story 4.2: Household Items CRUD API.

Reviewed files:

  • server/src/services/householdItemService.ts (new)
  • server/src/routes/householdItems.ts (new)
  • server/src/app.ts (modified — route registration only)
  • server/src/services/documentLinkService.ts (modified — household_item entity type support)

Summary

No blocking security issues found. The implementation follows established secure patterns from earlier EPICs. Two informational observations are noted below.


Review Checklist

  • No SQL/command/XSS injection vectors in new code
  • Authentication enforced on all new endpoints
  • No sensitive data exposed in logs, errors, or client responses
  • User input validated and sanitized at API boundaries
  • New dependencies: none
  • No hardcoded credentials or secrets
  • CORS configuration unchanged
  • Error responses do not leak internal details

Detailed Analysis

SQL Injection Prevention

LIKE query (q parameter): The search pattern in householdItemService.ts:492-498 correctly escapes LIKE wildcard metacharacters (% and _) before constructing the pattern string. The pattern is then passed to Drizzle's sql tagged template literal as a parameterized value (${pattern}), which binds it as a prepared statement parameter — not string interpolation into raw SQL. The ESCAPE '\\' clause is present and correctly referenced. This is the safe pattern.

sortBy column mapping: The listHouseholdItems function at lines 522-537 uses a hardcoded conditional chain that maps validated enum values to Drizzle column references. User-controlled sortBy string values never reach the SQL ORDER BY clause directly — only typed Drizzle column objects do. This is the correct whitelist approach and consistent with the pattern flagged as a security requirement in PR #396 memory notes.

All other queries: All INSERT, UPDATE, DELETE, and SELECT operations use Drizzle ORM's parameterized API (eq(), and(), inArray(), etc.). No raw string interpolation into SQL anywhere.

Authentication Enforcement

All five endpoints (POST, GET list, GET by ID, PATCH, DELETE) perform an explicit if (!request.user) throw new UnauthorizedError(...) guard before any business logic. All five endpoints are registered under the /api/household-items prefix in app.ts which receives the auth plugin. Consistent with the established baseline pattern.

Authorization (RBAC)

The implementation allows any authenticated user (member or admin) to perform all CRUD operations. This is consistent with the EPIC-04 design: household items are shared household data, not admin-only. The test at line 468 explicitly verifies member access to create, and line 1227 verifies member access to delete. No privilege escalation vectors.

Input Validation

Schema completeness:

  • name: minLength:1, maxLength:500 — correct
  • description: maxLength:5000 — correct
  • url: maxLength:2000 — correct
  • room: maxLength:200 — correct
  • quantity: integer, minimum:1 — correct
  • category, status: enum-constrained — correct
  • orderDate, expectedDeliveryDate, actualDeliveryDate: format:'date' — correct
  • additionalProperties: false on all body schemas — correct
  • minProperties: 1 on PATCH body — prevents no-op empty updates

Cascade delete: deleteHouseholdItem calls deleteLinksForEntity(db, 'household_item', id) before the main row deletion. The documentLinkService.deleteLinksForEntity uses parameterized Drizzle queries. This correctly implements the application-layer cascade documented in ADR-015.

documentLinkService entity validation: The household_item entity type is now handled in the createLink function (lines 90-95) with a DB existence check before insert. The documentLinks.test.ts update (400 → 404 for non-existent household_item) correctly reflects this.


Informational Findings

[Informational] q search parameter has no maxLength bound

Location: server/src/routes/householdItems.ts:79

q: { type: 'string' }

The q search parameter has no maxLength constraint. An authenticated user could submit a very long search string (e.g., 10 MB), which would be escaped and used as a LIKE pattern. Since this is a single-tenant self-hosted application with authenticated access only, the practical impact is negligible — no external attacker can reach this endpoint. However, for consistency with the defense-in-depth approach applied to other string fields (name, description, url, room), adding a reasonable bound (e.g., maxLength: 200) would be appropriate. This is consistent with open finding #7 in the known backlog.

[Informational] tagIds array has no maxItems bound

Location: server/src/routes/householdItems.ts:42-46 and 130-134

tagIds: {
  type: 'array',
  items: { type: 'string' },
  uniqueItems: true
}

The tagIds array has no maxItems constraint. In createHouseholdItem and updateHouseholdItem, each tag ID triggers an individual SELECT query in validateTagIds (one query per tag). This is the same N+1 pattern noted in open finding #16 (workItemIds in milestones). For a self-hosted single-tenant application with authenticated access and a small expected tag count, the risk is negligible. Adding maxItems: 50 would bound the worst case.

Both informational items are consistent with patterns already tracked in the security backlog (GitHub Issue #315). No new tracking items are required.


Conclusion

No critical, high, medium, or low findings. The implementation correctly applies all established security patterns: parameterized queries, enum-constrained sort column, LIKE metacharacter escaping with proper parameterization, explicit auth guards on every endpoint, additionalProperties: false on request bodies, and application-layer cascade delete for document links. This PR is clear to merge.

@steilerDev steilerDev merged commit 0224a30 into beta Mar 3, 2026
9 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 3, 2026

🎉 This PR is included in version 1.12.0-beta.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 7, 2026

🎉 This PR is included in version 1.12.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@steilerDev steilerDev deleted the feat/388-household-items-crud-api branch March 7, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants