feat(work-items): implement notes and subtasks API (#90)#102
Conversation
Adds 9 new API endpoints for managing work item notes and subtasks: - Notes: POST/GET/PATCH/DELETE /api/work-items/:workItemId/notes[/:noteId] - Subtasks: POST/GET/PATCH/DELETE/reorder /api/work-items/:workItemId/subtasks[/:subtaskId] Includes author/admin authorization for note editing/deletion, automatic sort order assignment for subtasks, and bulk reorder endpoint. Fixes #90 Co-Authored-By: Claude backend-developer (Opus 4.6) <noreply@anthropic.com>
…ks API Adds comprehensive test coverage for noteService, subtaskService, note routes, and subtask routes including authorization checks, validation, CRUD operations, and subtask reordering. - noteService.test.ts: 29 unit tests covering create, list, update, delete - subtaskService.test.ts: 42 unit tests covering create, list, update, delete, reorder - notes.test.ts: 16 integration tests covering all REST endpoints with auth - subtasks.test.ts: 27 integration tests covering all REST endpoints with auth Total new tests: 114 tests Total test suite: 912 tests pass (42 suites) Relates to #90 Co-Authored-By: Claude qa-integration-tester (Opus 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude orchestrator (Opus 4.6) <noreply@anthropic.com>
steilerDev
left a comment
There was a problem hiding this comment.
[product-architect] Architecture review of PR #102 — Story 3.4: Work Item Notes & Subtasks API
Summary
This PR implements 9 new API endpoints (4 for notes, 5 for subtasks) with proper service-layer separation, author/admin authorization for notes, auto sort-order for subtasks, and comprehensive test coverage (114 new tests, 912 total). The implementation follows established patterns from the existing work items and tags CRUD routes.
Architecture Compliance
PASS — The implementation follows our established Fastify plugin patterns correctly:
- Route modules registered in
app.tswith nested URL prefixes (/api/work-items/:workItemId/notes,/api/work-items/:workItemId/subtasks) - Service layer contains all business logic; route handlers delegate properly
- Service functions accept
dbas first parameter (dependency injection pattern consistent withworkItemService.tsandtagService.ts) - Type imports from
@cornerstone/sharedusingimport type(ESM convention) - Error handling via
AppErrorsubclasses (NotFoundError,ValidationError,ForbiddenError) — consistent with the error handler plugin
Good design decisions:
- The
/reorderroute is registered BEFORE/:subtaskIdto avoid route parameter collision (Fastify processes routes in registration order). This is correctly documented with an inline comment. ensureWorkItemExists()helper avoids duplicating the work item existence check across all service functions.- Input trimming (whitespace) with post-trim emptiness validation is a nice defense-in-depth alongside the AJV
minLength: 1schema check.
API Contract Alignment
I checked all 9 endpoints against the Wiki API Contract:
| Endpoint | HTTP | Status Codes | Request/Response Shape | Verdict |
|---|---|---|---|---|
| POST .../notes | 201/400/401/404 | Matches | Matches | PASS |
| GET .../notes | 200/401/404 | Matches | Matches | PASS |
| PATCH .../notes/:noteId | 200/400/401/403/404 | Matches | Matches | PASS |
| DELETE .../notes/:noteId | 204/401/403/404 | Matches | Matches | PASS |
| POST .../subtasks | 201/400/401/404 | Matches | Matches | PASS |
| GET .../subtasks | 200/401/404 | Matches | Matches | PASS |
| PATCH .../subtasks/:subtaskId | 200/400/401/404 | Matches | Matches | PASS |
| DELETE .../subtasks/:subtaskId | 204/401/404 | Matches | Matches | PASS |
| PATCH .../subtasks/reorder | 200/400/401/404 | Matches | See issue below | ISSUE |
Shared types match the contract. NoteResponse, NoteUserSummary, SubtaskResponse, CreateNoteRequest, UpdateNoteRequest, CreateSubtaskRequest, UpdateSubtaskRequest, and ReorderSubtasksRequest in @cornerstone/shared all align with the Wiki API Contract response shapes.
Issues Requiring Attention
1. CRITICAL: Reorder endpoint allows partial reordering (contract violation)
The Wiki API Contract explicitly states for PATCH /api/work-items/:workItemId/subtasks/reorder:
subtaskIds: "Must contain ALL subtask IDs belonging to this work item (no subset reordering)."
However, the implementation in subtaskService.ts (reorderSubtasks()) only validates that provided IDs belong to the work item — it does NOT validate that ALL subtask IDs are included. The test at subtaskService.test.ts line "handles partial reordering (subset of subtasks)" explicitly tests and passes this partial reordering scenario.
The fix is straightforward: Add a validation check that data.subtaskIds.length === existingSubtasks.length (in addition to the existing ID membership check). If they don't match, throw ValidationError('subtaskIds must contain all subtask IDs for this work item'). The existing test "handles partial reordering" should be changed to expect a ValidationError instead of success.
This is the only blocking issue.
2. OBSERVATION: Note content maxLength not enforced in AJV schema
The Wiki API Contract specifies note content as "1-10000 characters", but the AJV JSON schemas for createNoteSchema and updateNoteSchema only specify minLength: 1 — no maxLength: 10000. Similarly, subtask title is specified as "1-500 characters" but the AJV schema only has minLength: 1.
This is a defense-in-depth concern. The service layer trim + empty check handles the minimum, but very long content (e.g., a 1MB string) would be accepted. Consider adding maxLength to match the contract:
// notes
content: { type: 'string', minLength: 1, maxLength: 10000 }
// subtasks
title: { type: 'string', minLength: 1, maxLength: 500 }This is consistent with workItems.ts which uses maxLength: 500 for title and maxLength: 10000 for description.
3. OBSERVATION: Duplicate ensureWorkItemExists() function
Both noteService.ts and subtaskService.ts define identical ensureWorkItemExists() functions. This is not blocking but could be extracted to a shared utility (e.g., server/src/services/workItemHelpers.ts) during the refinement phase.
Code Quality
PASS — Clean, consistent code:
- Proper
typeimports throughout (import type { ... }) - Consistent use of
.jsextensions in import paths (ESM requirement) DbTypealias follows the pattern fromworkItemService.ts- snake_case in DB columns, camelCase in TypeScript — naming conventions respected
- Error messages are descriptive and match the API contract
- No
anytypes - Parameterized queries via Drizzle ORM (no SQL injection risk)
Test Coverage
PASS — 114 new tests across 4 test files:
| File | Tests | Coverage |
|---|---|---|
noteService.test.ts |
29 unit tests | CRUD + auth + edge cases |
subtaskService.test.ts |
42 unit tests | CRUD + reorder + edge cases |
notes.test.ts |
16 integration tests | HTTP endpoints + auth |
subtasks.test.ts |
27 integration tests | HTTP endpoints + auth + reorder |
Test quality is high:
- UAT scenario IDs are cross-referenced in test names (e.g.,
(UAT-3.4-01)) - Tests cover all authorization paths (author, admin, non-author non-admin)
- Edge cases tested: empty content, non-existent resources, cross-work-item access, null createdBy (deleted user)
- Timestamp handling for sort order tests uses offsets to ensure deterministic ordering
- Integration tests use
app.inject()(no HTTP server needed) — consistent with existing test patterns
Verdict
Approve with one required fix:
- REQUIRED: Fix the reorder endpoint to reject partial subtask ID arrays per the API contract. This is a contract violation that must be corrected before merge.
- RECOMMENDED (refinement): Add
maxLengthconstraints to AJV schemas for notes content (10000) and subtask title (500). - RECOMMENDED (refinement): Extract shared
ensureWorkItemExists()to avoid duplication.
The overall quality of this PR is strong. The service layer separation, authorization model, and test coverage are all well-executed.
steilerDev
left a comment
There was a problem hiding this comment.
[product-owner]
PR #102 Review: Story 3.4 — Work Item Notes & Subtasks API (#90)
Acceptance Criteria Verification
| AC | Description | Verdict | Evidence |
|---|---|---|---|
| 1 | POST notes returns 201, auto-sets createdBy, 404 for missing work item |
PASS | noteService.createNote() sets createdBy from authenticated user; additionalProperties: false prevents client override; 404 NOT_FOUND thrown by ensureWorkItemExists(). Tests: UAT-3.4-01, 02, 04. |
| 2 | GET notes sorted created_at DESC, includes user summary {id, displayName} |
PASS | listNotes() uses orderBy(desc(workItemNotes.createdAt)) with LEFT JOIN on users. Response: { notes: [...] }. Tests: UAT-3.4-07, 08. |
| 3 | PATCH notes: author/admin can edit, 403 for others, 404 for missing | PASS | updateNote() checks note.createdBy !== userId && !isAdmin before update. Correct error codes. Tests: UAT-3.4-09 through 13. |
| 4 | DELETE notes: author/admin can delete, 403/404 | PASS | deleteNote() with identical authorization pattern. Returns 204. Tests: UAT-3.4-14 through 17. |
| 5 | POST subtasks: sortOrder auto-appends via MAX(sort_order) + 1 |
PASS | getNextSortOrder() uses COALESCE(MAX(sort_order), -1) + 1. Returns 201. Tests: UAT-3.4-18 through 21. |
| 6 | GET subtasks sorted sort_order ASC |
PASS | listSubtasks() uses orderBy(asc(workItemSubtasks.sortOrder)). Response: { subtasks: [...] }. Tests: UAT-3.4-22, 23. |
| 7 | PATCH subtasks: title, isCompleted, sortOrder partial update |
PASS | updateSubtask() handles partial updates; minProperties: 1 in schema rejects empty body. Tests: UAT-3.4-24 through 28. |
| 8 | DELETE subtasks: 204/404 | PASS | deleteSubtask() validates work item ownership, returns 204. Tests: UAT-3.4-29, 30. |
| 9 | PATCH reorder: ordered array of subtask IDs, 400 for invalid IDs | PASS | reorderSubtasks() validates all IDs belong to work item, updates sort_order to array index. /reorder registered before /:subtaskId to avoid parameter collision. Tests: UAT-3.4-31 through 33. |
| 10 | All endpoints require authentication (401) | PASS | Every route handler checks !request.user and throws UnauthorizedError(). All 9 endpoints have explicit 401 tests. |
| 11 | Validation errors return 400 VALIDATION_ERROR |
PASS | AJV schema enforces minLength: 1, type: 'integer', minItems: 1. Service layer validates whitespace-only strings after trim. |
All 11 acceptance criteria are met.
UAT Scenario Alignment
114 new tests cover the core UAT scenarios. Non-blocking observations on UAT gaps:
-
UAT-3.4-03 / UAT-3.4-26 (max length limits: 10,000 chars for note content, 500 chars for subtask title): The implementation does not enforce max length constraints. The ACs specify "non-empty" but not a max length, so the UAT scenarios went beyond the ACs. This is acceptable for now — max length validation can be added in refinement if desired.
-
UAT-3.4-43 (reorder requires ALL subtask IDs): The implementation allows partial reordering (only a subset of subtask IDs), which is tested and functional. The UAT scenario expected all IDs to be required. The AC says "accepts an ordered array of subtask IDs" without specifying all-or-nothing. The partial reorder is actually more flexible and useful for frontend drag-and-drop. Flag for refinement discussion.
-
UAT-3.4-49 (deleted user graceful handling): Tested at service level.
createdBycorrectly returnsnullwhen the author user is deleted (SET NULL FK). Not tested at route/integration level but behavior is correct. -
UAT-3.4-50, 3.4-51 (edge cases: sort order gaps, special characters): Not explicitly tested but handled naturally by the implementation (ORDER BY handles gaps; SQLite stores content as-is).
Scope Assessment
The PR stays within the story's scope — purely backend API endpoints for notes and subtasks. No frontend code, no schema changes (schema was done in Story 3.1), no scope creep.
Code Quality Observations
- Clean service/route separation following the established pattern from Stories 3.2 and 3.3
- Proper use of Drizzle ORM with parameterized queries (no SQL injection risk)
- Work item ownership validation on all note/subtask operations (prevents cross-work-item access via URL manipulation)
- Content trimming with post-trim emptiness checks catches whitespace-only input
- Route registration order for
/reorderbefore/:subtaskIdis documented and correct
CI Status
- Quality Gates: PASS (lint, typecheck, test, format, build, npm audit)
- Docker: PASS
- 912 total tests passing across 42 suites
Agent Responsibilities Status
| Agent | Status |
|---|---|
| Backend Developer | DONE — 8 new files, 3,320 additions |
| QA Integration Tester | DONE — 114 tests (29 + 42 service, 16 + 27 integration) |
| Product Architect | PENDING — review not yet posted |
| Security Engineer | PENDING — review not yet posted |
Decision
CONDITIONAL APPROVE — All 11 acceptance criteria are verified and met. CI is green. Test coverage is thorough. Approval is conditional on the product-architect and security-engineer completing their reviews. Once both reviews are posted (approve or request-changes), this PR is ready to merge from the product perspective.
Non-blocking items for refinement backlog:
- Add
maxLengthvalidation for note content (10,000) and subtask title (500) per UAT scenarios - Decide on partial vs. full reorder semantics (UAT-3.4-43)
- Add route-level integration test for deleted user graceful handling (UAT-3.4-49)
steilerDev
left a comment
There was a problem hiding this comment.
[security-engineer] Security review completed for Story 3.4 — Work Item Notes & Subtasks API (PR #102)
Security Review Summary
Overall Assessment: APPROVED with MEDIUM-SEVERITY RECOMMENDATIONS
This PR implements 9 new API endpoints for managing work item notes and subtasks. The implementation demonstrates strong security practices with proper authorization controls, SQL injection prevention, and IDOR protection. However, there are input validation gaps that should be addressed to prevent potential DoS attacks.
✅ Security Controls Verified
1. Authorization (OWASP A01 - Broken Access Control)
Notes Authorization ✓
- Author/Admin pattern correctly implemented: Only note authors and admins can update/delete notes
- Service layer enforces authorization:
note.createdBy \!== userId && \!isAdminchecks present - Integration tests verify authorization (UAT-3.4-09, 3.4-10, 3.4-11, 3.4-14, 3.4-15, 3.4-16)
- 403 responses correctly returned for unauthorized access
Subtasks Authorization ✓
- All authenticated users can create/read/update/delete subtasks (no author restriction)
- Consistent with requirements and appropriate for checklist functionality
Authentication ✓
- All 9 endpoints check
request.userand throwUnauthorizedErrorif missing - Integration tests verify 401 responses for unauthenticated requests
2. IDOR Prevention (OWASP A01) ✓
Excellent protection against cross-work-item access:
- Both services verify
note.workItemId \!== workItemId/subtask.workItemId \!== workItemId - Attempts to access notes/subtasks via wrong work item ID return 404 (not 403, preventing enumeration)
- Unit tests specifically cover cross-work-item access attempts:
noteService.test.ts: Lines 397, 500subtaskService.test.ts: Lines 423, 513, 618
3. SQL Injection Prevention (OWASP A03) ✓
All queries use Drizzle ORM parameterized queries:
db.select().from(table).where(eq(column, param))pattern throughout- No raw SQL strings or string concatenation
- User input never directly interpolated into queries
- 100% safe from SQL injection
4. Input Validation (OWASP A03) ✓ (with gaps — see recommendations)
Schema Validation Present:
- JSON schema validation on all request bodies
minLength: 1enforced on content/title fieldsadditionalProperties: falseprevents extra fieldsminItems: 1on reorder array
Service Layer Validation:
- Content/title trimming and empty-string checks
- Work item existence verification before all operations
- Reorder endpoint validates all IDs belong to the work item
5. Sensitive Data Exposure (OWASP A02) ✓
- No sensitive data in note/subtask responses
- User summary includes only
idanddisplayName(no email, passwordHash, etc.) toNoteUserSummary()helper properly sanitizes user data
6. Error Message Security ✓
- Generic error messages prevent enumeration
- Cross-work-item access returns 404 (not 403) to prevent disclosure
- Authorization failures return 403 with clear but non-exploitable messages
⚠️ Recommendations (Medium Severity)
1. Missing maxLength Constraints (Medium)
Issue: Notes content and subtask titles lack maximum length constraints, creating DoS risk.
Current State:
notes.ts:content: { type: 'string', minLength: 1 }(no maxLength)subtasks.ts:title: { type: 'string', minLength: 1 }(no maxLength)
Comparison: Work items enforce reasonable limits:
title: { maxLength: 500 }description: { maxLength: 10000 }
Risk: An attacker could:
- Submit multi-megabyte note content
- Create subtasks with extremely long titles
- Exhaust database storage or cause memory issues during query processing
- Impact application performance for all users
Recommendation: Add maxLength constraints to all schemas:
// notes.ts
content: { type: 'string', minLength: 1, maxLength: 10000 }
// subtasks.ts
title: { type: 'string', minLength: 1, maxLength: 500 }Justification: 10000 chars for note content (matches work item description), 500 chars for subtask titles (matches work item titles). These limits are generous for legitimate use cases while preventing abuse.
2. Missing maxItems on Reorder Array (Medium)
Issue: reorderSubtasksSchema.subtaskIds array has no maxItems constraint.
Risk: An attacker could submit an array with thousands/millions of IDs, causing:
- Database query performance degradation
- Memory exhaustion during validation loop
- CPU exhaustion during N individual UPDATE queries
Current Logic: data.subtaskIds.forEach((subtaskId, index) => { db.update(...) })
- Each ID triggers a separate database UPDATE
- No batching or limit on array size
Recommendation: Add maxItems constraint:
subtaskIds: {
type: 'array',
items: { type: 'string' },
minItems: 1,
maxItems: 1000, // Reasonable limit for subtasks per work item
}Note: If practical subtask counts are expected to be much lower (e.g., 10-50), consider a lower limit like 100.
3. Reorder Performance Improvement (Low)
Non-security observation: The reorder logic executes N individual UPDATE queries instead of using a batch update or transaction. While not a security issue per se, this could be optimized to reduce database load and improve atomicity. Consider using a transaction wrapper or Drizzle's batch update API in a future refactoring task.
🔍 Testing Coverage Assessment
Unit Tests: 71 tests (29 noteService + 42 subtaskService) ✓
- Authorization logic thoroughly tested
- Cross-work-item access blocked
- Edge cases covered
Integration Tests: 43 tests (16 note routes + 27 subtask routes) ✓
- End-to-end authorization flows verified
- 401/403/404 responses tested
- UAT scenarios mapped to tests
Security-Specific Tests Verified:
- Note author-only update (UAT-3.4-09, 3.4-10, 3.4-11)
- Note author-only delete (UAT-3.4-14, 3.4-15, 3.4-16)
- Cross-work-item IDOR attempts (noteService 397, 500; subtaskService 423, 513, 618)
✅ OWASP Top 10 Compliance Check
| Category | Status | Notes |
|---|---|---|
| A01 - Broken Access Control | ✅ PASS | Author/admin authorization working, IDOR prevented |
| A02 - Cryptographic Failures | ✅ PASS | No cryptographic operations, no sensitive data exposure |
| A03 - Injection | ✅ PASS | Drizzle ORM parameterized queries throughout |
| A04 - Insecure Design | ✅ PASS | Authorization design is sound |
| A05 - Security Misconfiguration | Missing input length constraints (see recommendations) | |
| A06 - Vulnerable Components | ✅ PASS | No new dependencies introduced |
| A07 - Auth Failures | ✅ PASS | Authentication enforced on all endpoints |
| A08 - Data Integrity | ✅ PASS | No integrity risks identified |
| A09 - Logging Failures | ✅ PASS | Standard error handling present |
| A10 - SSRF | ✅ PASS | No external requests made |
📋 Security Checklist
- No SQL/command/XSS injection vectors in new code
- Authentication/authorization enforced on all new endpoints
- No sensitive data exposed in logs, errors, or client responses
- User input validated at API boundaries (with length gaps noted above)
- No new dependencies with known CVEs
- No hardcoded credentials or secrets
- Error responses do not leak internal details
- IDOR vulnerabilities prevented
🎯 Verdict
APPROVED FOR MERGE with the recommendation that the implementing agent address the input validation gaps in a follow-up commit before the PR is merged, or document them as technical debt for the refinement phase.
The core security controls (authorization, IDOR prevention, SQL injection prevention) are correctly implemented and well-tested. The missing length constraints are a preventable DoS risk but not immediately exploitable in a 1-5 user self-hosted environment. However, following defense-in-depth principles, these should be added before the feature ships.
Suggested Action: Backend developer should add maxLength constraints to note content, subtask title, and maxItems to reorder array, then update this PR. Estimated effort: 10 minutes.
… validation Addresses review feedback: - reorderSubtasks now requires ALL subtask IDs (API contract compliance) - Added maxLength constraints for note content (10000) and subtask title (500) - Added maxItems: 1000 for reorder array Co-Authored-By: Claude backend-developer (Opus 4.6) <noreply@anthropic.com>
|
🎉 This PR is included in version 1.8.0-beta.5 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
|
🎉 This PR is included in version 1.8.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
/api/work-items/:workItemId/notes[/:noteId]with author/admin authorization/api/work-items/:workItemId/subtasks[/:subtaskId]with auto sort-orderAcceptance Criteria
Test plan
Fixes #90
🤖 Generated with Claude Code