Skip to main content

Feature: Notes (Catch-All)

Version: 1.1.0 Last Reviewed: 2026-02-18 Status: Approved

User Story

As a caregiver, I can log a free-form note with a category so we can track things not yet supported (pumping, meds, health observations).

MVP Scope

  • Timestamp (defaults to "now", editable)
  • Category: fixed list (pumping, health, medication, other)
  • Text: free-form, required, max 10,000 characters
  • Title: optional, max 200 characters
  • Full CRUD operations
  • Category-based filtering on list
  • Time-based filtering on list
  • Integration with timeline API (specified in timeline-api.md)

NOT in MVP

  • Custom categories (Post-MVP #8)
  • Attachments, photos, rich text
  • Note templates
  • Full-text search across note text
  • Note reminders or scheduling
  • Markdown formatting support

API Contract

Create Note

POST /api/v1/children/:childId/notes
Authorization: Bearer <token>

Request:
{
"category": "medication",
"text": "Gave 2ml of vitamin D drops"
}

Request (with title and custom timestamp):
{
"timestamp": "2026-02-10T14:30:00.000Z",
"category": "pumping",
"title": "Morning pump session",
"text": "Pumped 4oz from left side, 3.5oz from right."
}

Response 201:
{
"note": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"child_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2026-02-10T14:30:00.000Z",
"category": "pumping",
"title": "Morning pump session",
"text": "Pumped 4oz from left side, 3.5oz from right.",
"created_by": "789e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-02-10T14:30:15.000Z",
"updated_by": "789e4567-e89b-12d3-a456-426614174000",
"updated_at": "2026-02-10T14:30:15.000Z"
}
}

Get Note by ID

GET /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>

Response 200:
{
"note": { ... }
}

List Notes

GET /api/v1/children/:childId/notes?category=pumping&since=2026-02-10T00:00:00.000Z
Authorization: Bearer <token>

Query Parameters (all optional):
- since: ISO8601 timestamp - return notes after this time (exclusive)
- category: filter by category (pumping, health, medication, other)
- limit: integer - max results (default 100, max 500)

Response 200:
{
"notes": [
{
"id": "...",
"child_id": "...",
"timestamp": "2026-02-10T18:45:00.000Z",
"category": "health",
"title": null,
"text": "First tooth visible on lower gum!",
"created_by": "...",
"created_at": "...",
"updated_by": "...",
"updated_at": "..."
}
],
"count": 1
}

Note: Results ordered by timestamp DESC (newest first).

Update Note

PUT /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>

Request (all fields optional - partial update):
{
"text": "Updated text",
"category": "health",
"title": "Updated title"
}

Response 200:
{
"note": { ... }
}

Delete Note

DELETE /api/v1/children/:childId/notes/:id
Authorization: Bearer <token>

Response 204: (No Content)

Error Responses

Standard error format:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed",
"details": [
{ "field": "text", "message": "Required and cannot be empty", "code": "required" }
]
}
}

Error codes: 400 VALIDATION_ERROR, 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND

Business Rules

Validation

  1. category: REQUIRED. Must be exactly "pumping", "health", "medication", or "other" (case-sensitive).
  2. text: REQUIRED. Min 1 character after trimming whitespace. Max 10,000 characters. Whitespace is trimmed on storage. Supports UTF-8 including emojis.
  3. title: Optional. Max 200 characters. Empty string after trim is treated as null. Stored trimmed.
  4. timestamp: Optional, defaults to server's current time. Must be valid ISO8601 if provided.

Access Control

  • All operations require authentication
  • User must have ChildAccess record (owner or caregiver) for the child
  • No access → 403 FORBIDDEN
  • Child/note not found → 404 NOT_FOUND

Audit Trail

  • All CUD operations create an AuditLog entry
  • Create: changes = full entity
  • Update: changes = { before: {...}, after: {...} }
  • Delete: changes = full entity being deleted

Query Behavior

  • List orders by timestamp descending, ties broken by created_at descending
  • "since" filters exclusively (timestamp > since)
  • Category filter is exact match, case-sensitive
  • Combined filters use AND logic
  • Default limit 100, max 500
  • Responses wrapped: { "notes": [...], "count": N }

Acceptance Criteria

  • Can create notes with each category (pumping, health, medication, other)
  • Timestamp defaults to now if not provided
  • Can get a single note by ID
  • Can list notes with category filter
  • Can list notes with since filter
  • Can combine category + since filters
  • Can edit text, category, and timestamp
  • Can delete a note (returns 204)
  • Text is trimmed on storage
  • Empty/whitespace-only text returns 400
  • Text max 10,000 characters enforced
  • UTF-8 and emojis preserved correctly
  • List returns notes wrapped with count
  • Validation errors return 400 with details array
  • No access returns 403, not found returns 404
  • All CUD operations create AuditLog entries

Test Cases

Create - Happy Path

  1. Pumping note: category=pumping, text="..." → 201
  2. Health note: category=health → 201
  3. Medication note: category=medication → 201
  4. Other note: category=other → 201
  5. Custom timestamp: timestamp=past date → 201, timestamp matches
  6. No timestamp: → 201, server sets current time
  7. Long text (5000 chars): → 201, full text stored
  8. UTF-8 and emojis: text="今日は 😊👶" → 201, preserved

Create - Validation

  1. Missing category: {text: "..."} → 400
  2. Invalid category: category=custom → 400
  3. Category case sensitive: category=Pumping → 400
  4. Missing text: {category: "health"} → 400
  5. Empty text: text="" → 400
  6. Whitespace only text: text=" " → 400 (trimmed to empty)
  7. Text too long: 10,001 chars → 400
  8. Text at max: 10,000 chars → 201

Create - Access Control

  1. No auth: → 401
  2. No access: → 403
  3. Caregiver access: → 201
  4. Child not found: → 404

Get by ID

  1. Get existing note: → 200, { note: {...} }
  2. Not found: → 404
  3. No access: → 403

List

  1. List all: → 200, { notes: [...], count: N }
  2. Filter by category: category=pumping → only pumping notes
  3. Filter by since: since=T1 → notes after T1 only (exclusive)
  4. Combined filters: category=pumping&since=T1 → AND logic
  5. List empty: → 200, { notes: [], count: 0 }
  6. List with limit: 5 notes, limit=2 → 2 most recent
  7. Invalid category filter: category=invalid → 400
  8. Ordering: newest first, ties by created_at

Update

  1. Update text: → 200
  2. Update category: → 200
  3. Update timestamp: → 200
  4. Update all fields: → 200
  5. Partial update: only text → category unchanged
  6. Text trimmed on update: " spaces " → "spaces"
  7. Empty text on update: text="" → 400
  8. Audit log: before/after captured
  9. Not found: → 404
  10. No access: → 403

Delete

  1. Delete note: → 204
  2. Delete then list: note gone
  3. Not found: → 404
  4. No access: → 403
  5. Audit log: deleted entity captured

Edge Cases

  1. Newlines preserved: text with \n → stored correctly
  2. Future timestamp: → 201 (allowed)
  3. Distant past timestamp: → 201 (allowed)
  4. Concurrent updates: last write wins, both audited

Boundaries

  • No custom categories (fixed list for MVP)
  • No full-text search
  • No attachments or rich text
  • No pagination beyond limit parameter
  • Timeline integration defined in timeline-api.md, not here
  • Category filter is case-sensitive exact match