Skip to content

feat(work-items): implement notes and subtasks API (#90)#102

Merged
steilerDev merged 4 commits into
betafrom
feat/90-notes-subtasks-api
Feb 17, 2026
Merged

feat(work-items): implement notes and subtasks API (#90)#102
steilerDev merged 4 commits into
betafrom
feat/90-notes-subtasks-api

Conversation

@steilerDev
Copy link
Copy Markdown
Owner

Summary

  • Implements 9 new API endpoints for managing work item notes and subtasks
  • Notes: POST/GET/PATCH/DELETE /api/work-items/:workItemId/notes[/:noteId] with author/admin authorization
  • Subtasks: POST/GET/PATCH/DELETE/reorder /api/work-items/:workItemId/subtasks[/:subtaskId] with auto sort-order
  • 114 new tests (29 note service + 42 subtask service + 16 note routes + 27 subtask routes)
  • 912 total tests passing across 42 suites

Acceptance Criteria

  • AC1: POST notes with 201, createdBy auto-set, 404 for missing work item
  • AC2: GET notes sorted by created_at DESC with user summary
  • AC3: PATCH notes with author/admin authorization, 403 for others
  • AC4: DELETE notes with author/admin authorization
  • AC5: POST subtasks with auto sort-order assignment
  • AC6: GET subtasks sorted by sort_order ASC
  • AC7: PATCH subtasks (title, isCompleted, sortOrder)
  • AC8: DELETE subtasks
  • AC9: PATCH reorder with ordered array validation
  • AC10: All endpoints require authentication
  • AC11: Validation errors return 400

Test plan

  • Unit tests for noteService (29 tests)
  • Unit tests for subtaskService (42 tests)
  • Integration tests for note routes (16 tests)
  • Integration tests for subtask routes (27 tests)
  • Authorization tests (author/admin/other-user)
  • Lint, typecheck, format, build all pass

Fixes #90

🤖 Generated with Claude Code

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>
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 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.ts with 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 db as first parameter (dependency injection pattern consistent with workItemService.ts and tagService.ts)
  • Type imports from @cornerstone/shared using import type (ESM convention)
  • Error handling via AppError subclasses (NotFoundError, ValidationError, ForbiddenError) — consistent with the error handler plugin

Good design decisions:

  • The /reorder route is registered BEFORE /:subtaskId to 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: 1 schema 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 type imports throughout (import type { ... })
  • Consistent use of .js extensions in import paths (ESM requirement)
  • DbType alias follows the pattern from workItemService.ts
  • snake_case in DB columns, camelCase in TypeScript — naming conventions respected
  • Error messages are descriptive and match the API contract
  • No any types
  • 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:

  1. 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.
  2. RECOMMENDED (refinement): Add maxLength constraints to AJV schemas for notes content (10000) and subtask title (500).
  3. 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.

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 #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:

  1. 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.

  2. 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.

  3. UAT-3.4-49 (deleted user graceful handling): Tested at service level. createdBy correctly returns null when the author user is deleted (SET NULL FK). Not tested at route/integration level but behavior is correct.

  4. 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 /reorder before /:subtaskId is 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 maxLength validation 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)

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 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 && \!isAdmin checks 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.user and throw UnauthorizedError if 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, 500
    • subtaskService.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: 1 enforced on content/title fields
  • additionalProperties: false prevents extra fields
  • minItems: 1 on 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 id and displayName (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 ⚠️ MEDIUM 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>
@steilerDev steilerDev merged commit 38ef221 into beta Feb 17, 2026
4 checks passed
@steilerDev steilerDev deleted the feat/90-notes-subtasks-api branch February 17, 2026 09:44
@steilerDev steilerDev mentioned this pull request Feb 17, 2026
11 tasks
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.8.0-beta.5 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.8.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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