foundary-test-1770784989/.sdlc/features/data-models/tasks.md
rdev-worker b4045b10ac
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
build: /breakdown-feature data-models
2026-02-11 05:11:11 +00:00

17 KiB

Tasks: Core Data Models & Persistence

Task Order (dependency sequence)

T1 (migrations) ──┐
                   ├──► T4 (ports) ──► T5 (adapters)
T2 (domain) ──────┤                        │
    │              └──► T6 (project/label svc) ──► T9 (project/label handlers)
    ▼                                                          │
T3 (domain tests)  T7 (task/assoc svc) ──► T10 (task/assoc handlers)
                        │                          │
                   T8 (svc tests)          T11 (handler tests)
                                                   │
                                           T12 (routes, spec, wiring)

Dependency chain depth: 5 (T2 → T4 → T6/T7 → T9/T10 → T12)


T1: SQL Migrations (task-001)

  • Scope: Create three PostgreSQL migration files that define the database schema for all five tables (projects, tasks, labels, task_labels, assignments) with proper FK constraints, indexes, and unique constraints.
  • Files:
    • services/studio-api/migrations/001_create_projects.sql (new)
    • services/studio-api/migrations/002_create_tasks.sql (new)
    • services/studio-api/migrations/003_create_labels_and_associations.sql (new)
  • Depends on: None
  • Acceptance criteria:
    • 001_create_projects.sql creates projects table with UUID PK, name (VARCHAR 100, UNIQUE, NOT NULL), description (VARCHAR 500), created_at/updated_at (TIMESTAMPTZ with defaults)
    • 002_create_tasks.sql creates tasks table with UUID PK, project_id FK (CASCADE), title, description, status, priority, timestamps; indexes on project_id, status, priority
    • 003_create_labels_and_associations.sql creates labels, task_labels (composite PK), and assignments tables with proper FK CASCADE constraints and indexes
    • All tables use UUID primary keys
    • FK columns have ON DELETE CASCADE
    • Indexes exist on: tasks.project_id, tasks.status, tasks.priority, task_labels.label_id, assignments.task_id, assignments.user_id
    • Unique constraints on: projects.name, labels.name, assignments(task_id, user_id)

T2: Domain Entities and Errors (task-002)

  • Scope: Create domain types for Project, Task, Label, Assignment, and TaskLabel following the existing Example entity pattern. Each entity gets a strong-typed ID, a constructor with validation, and an Update method where applicable. Add all domain error sentinels to errors.go.
  • Files:
    • services/studio-api/internal/domain/project.go (new)
    • services/studio-api/internal/domain/task.go (new)
    • services/studio-api/internal/domain/label.go (new)
    • services/studio-api/internal/domain/assignment.go (new)
    • services/studio-api/internal/domain/task_label.go (new)
    • services/studio-api/internal/domain/errors.go (modify)
  • Depends on: None
  • Acceptance criteria:
    • ProjectID strong type; NewProject(name, description) validates name (1-100 chars), description (max 500); Update(name, description) method
    • TaskID, TaskStatus, TaskPriority strong types with const enums; NewTask(projectID, title, description, status, priority) validates all fields; Update(...) method; defaults status=open, priority=medium when empty
    • LabelID strong type; NewLabel(name, color) validates name (1-50 chars), color (optional, hex format ^#[0-9A-Fa-f]{6}$); Update(name, color) method
    • AssignmentID strong type; NewAssignment(taskID, userID) constructor
    • TaskLabel struct with NewTaskLabel(taskID, labelID) constructor
    • Domain errors added to errors.go: ErrProjectNotFound, ErrDuplicateProject, ErrInvalidProjectName, ErrTaskNotFound, ErrInvalidTaskTitle, ErrInvalidTaskStatus, ErrInvalidTaskPriority, ErrLabelNotFound, ErrDuplicateLabel, ErrInvalidLabelName, ErrInvalidLabelColor, ErrDuplicateTaskLabel, ErrTaskLabelNotFound, ErrDuplicateAssignment, ErrAssignmentNotFound
    • All constructors generate UUID v4 IDs and set timestamps to UTC now

T3: Domain Entity Tests (task-003)

  • Scope: Write table-driven unit tests for all domain entity constructors, validation logic, Update methods, and enum types.
  • Files:
    • services/studio-api/internal/domain/project_test.go (new)
    • services/studio-api/internal/domain/task_test.go (new)
    • services/studio-api/internal/domain/label_test.go (new)
  • Depends on: T2
  • Acceptance criteria:
    • Project tests: valid creation, empty name, name too long, description too long, update validation
    • Task tests: valid creation, empty title, title too long, invalid status, invalid priority, default status/priority when omitted, update validation
    • Label tests: valid creation, empty name, name too long, valid color formats, invalid color format, empty color (optional), update validation
    • All tests are table-driven
    • Tests pass with cd services/studio-api && go test -v ./internal/domain/...

T4: Port Interfaces (task-004)

  • Scope: Define repository interfaces for each entity following the ExampleRepository pattern. Each interface specifies the contract that adapters must implement.
  • Files:
    • services/studio-api/internal/port/project.go (new)
    • services/studio-api/internal/port/task.go (new)
    • services/studio-api/internal/port/label.go (new)
    • services/studio-api/internal/port/task_label.go (new)
    • services/studio-api/internal/port/assignment.go (new)
  • Depends on: T2
  • Acceptance criteria:
    • ProjectRepository: List, Get, Create, Update, Delete, ExistsByName methods with context.Context
    • TaskRepository: ListByProject (with optional status/priority filters), Get, Create, Update, Delete methods
    • LabelRepository: List, Get, Create, Update, Delete, ExistsByName methods
    • TaskLabelRepository: ListByTask, Attach, Detach, Exists methods
    • AssignmentRepository: ListByTask, Get, Create, Delete, ExistsByTaskAndUser methods
    • All methods accept context.Context as first parameter
    • Return types use domain types (not SQL/adapter types)
    • Documented error returns in comments

T5: PostgreSQL Adapters (task-005)

  • Scope: Implement all five repository interfaces using sqlx with raw SQL queries. Include PostgreSQL error detection for unique/FK constraint violations.
  • Files:
    • services/studio-api/internal/adapter/postgres/project.go (new)
    • services/studio-api/internal/adapter/postgres/task.go (new)
    • services/studio-api/internal/adapter/postgres/label.go (new)
    • services/studio-api/internal/adapter/postgres/task_label.go (new)
    • services/studio-api/internal/adapter/postgres/assignment.go (new)
    • services/studio-api/internal/adapter/postgres/errors.go (new — shared PG error helpers)
  • Depends on: T4
  • Acceptance criteria:
    • Each adapter struct takes *sqlx.DB and has a New*Repository(db) constructor
    • Compile-time interface verification: var _ port.XxxRepository = (*XxxRepository)(nil)
    • All SQL uses parameterized queries ($1, $2) — no string interpolation
    • isDuplicateKeyError() detects PG code 23505 and returns appropriate domain errors (ErrDuplicateProject, ErrDuplicateLabel, etc.)
    • isForeignKeyError() detects PG code 23503 and returns ErrProjectNotFound for invalid project_id
    • sql.ErrNoRows mapped to appropriate ErrXxxNotFound domain errors
    • Task listing supports optional WHERE clauses for status and priority filters
    • Row scanning uses sqlx struct tags (snake_case column mapping)

T6: Project and Label Services (task-006)

  • Scope: Create ProjectService and LabelService following the ExampleService pattern. Each service orchestrates CRUD operations with business logic (duplicate detection, validation, UUID generation).
  • Files:
    • services/studio-api/internal/service/project.go (new)
    • services/studio-api/internal/service/label.go (new)
  • Depends on: T2, T4
  • Acceptance criteria:
    • ProjectService with List, Get, Create, Update, Delete methods
    • ProjectService.Create checks ExistsByName before creating; returns ErrDuplicateProject on conflict
    • ProjectService.Update checks name conflicts (excluding self); returns ErrDuplicateProject on conflict
    • LabelService with List, Get, Create, Update, Delete methods
    • LabelService.Create checks ExistsByName before creating; returns ErrDuplicateLabel on conflict
    • LabelService.Update checks name conflicts (excluding self); returns ErrDuplicateLabel on conflict
    • Both services use CreateInput/UpdateInput DTOs (not domain types directly)
    • Both services accept port.XxxRepository and *slog.Logger via constructor
    • Structured logging with logger.WithService()

T7: Task and Association Services (task-007)

  • Scope: Create TaskService for task CRUD (with project FK validation) and AssociationService for task-label and task-user assignment operations.
  • Files:
    • services/studio-api/internal/service/task.go (new)
    • services/studio-api/internal/service/association.go (new)
  • Depends on: T2, T4
  • Acceptance criteria:
    • TaskService with List (by project, filterable), Get, Create, Update, Delete methods
    • TaskService.Create validates that the project exists (via ProjectRepository.Get) before creating; returns ErrProjectNotFound if not
    • TaskService.List accepts optional status and priority filter parameters
    • AssociationService with AttachLabel, DetachLabel, ListLabelsForTask, AssignUser, UnassignUser, ListAssignmentsForTask methods
    • AssociationService.AttachLabel verifies task and label exist before attaching; returns 404 if either missing, 409 if duplicate
    • AssociationService.AssignUser verifies task exists; returns 404 if missing, 409 if duplicate
    • Both services accept required repository ports via constructor

T8: Service Layer Tests (task-008)

  • Scope: Write unit tests for all four services using mock repositories. Follow the existing example_test.go pattern with local mock structs.
  • Files:
    • services/studio-api/internal/service/project_test.go (new)
    • services/studio-api/internal/service/task_test.go (new)
    • services/studio-api/internal/service/label_test.go (new)
    • services/studio-api/internal/service/association_test.go (new)
  • Depends on: T6, T7
  • Acceptance criteria:
    • Each test file defines local mock structs implementing the required port interfaces
    • Compile-time interface verification for all mocks
    • ProjectService tests: create (success, duplicate), get (found, not found), update (success, conflict, not found), delete (success, not found), list
    • TaskService tests: create (success, project not found, validation), get, update, delete, list with filters
    • LabelService tests: create (success, duplicate), get, update (conflict), delete, list
    • AssociationService tests: attach label (success, duplicate, task not found, label not found), detach, assign user (success, duplicate, task not found), unassign, list
    • All tests use logging.Nop() for no-op logger
    • Tests pass with cd services/studio-api && go test -v ./internal/service/...

T9: Project and Label HTTP Handlers (task-009)

  • Scope: Create handler structs for Project and Label CRUD operations following the Example handler pattern. Include request/response types, validation tags, response mapping, and mapDomainError functions.
  • Files:
    • services/studio-api/internal/api/handlers/project.go (new)
    • services/studio-api/internal/api/handlers/label.go (new)
  • Depends on: T6
  • Acceptance criteria:
    • Project handler: List, Get, Create, Update, Delete methods all returning error
    • CreateProjectRequest with Name (required, min=1, max=100) and Description (max=500) validation tags
    • UpdateProjectRequest with same validation
    • ProjectResponse struct with JSON tags; toProjectResponse() mapper
    • mapProjectDomainError() maps ErrProjectNotFound→404, ErrDuplicateProject→409, ErrInvalidProjectName→400
    • Label handler: List, Get, Create, Update, Delete methods
    • CreateLabelRequest with Name (required, min=1, max=50) and Color (optional) validation tags
    • mapLabelDomainError() maps label-specific domain errors
    • All handlers use app.BindAndValidate() for request bodies
    • All handlers use chi.URLParam(r, "id") with UUID validation for path params
    • Response: httpresponse.OK for GET/PUT, httpresponse.Created for POST, httpresponse.NoContent for DELETE

T10: Task and Association HTTP Handlers (task-010)

  • Scope: Create handler structs for Task CRUD and Association operations (task-labels, assignments). Tasks are nested under projects for creation/listing; associations are nested under tasks.
  • Files:
    • services/studio-api/internal/api/handlers/task.go (new)
    • services/studio-api/internal/api/handlers/association.go (new)
  • Depends on: T7
  • Acceptance criteria:
    • Task handler: List (reads projectId from URL + query params for status/priority), Get, Create (reads projectId from URL), Update, Delete
    • CreateTaskRequest with Title (required, min=1, max=200), Description (max=2000), Status (oneof enum), Priority (oneof enum) validation
    • mapTaskDomainError() maps ErrTaskNotFound→404, ErrProjectNotFound→404, ErrInvalidTaskTitle→400, ErrInvalidTaskStatus→400, ErrInvalidTaskPriority→400
    • Association handler: AttachLabel, DetachLabel, ListLabelsForTask, AssignUser, UnassignUser, ListAssignmentsForTask
    • AssignUserRequest with UserID (required) field
    • mapAssociationDomainError() maps all association errors (404, 409)
    • Task list handler reads ?status= and ?priority= query parameters via r.URL.Query()
    • All URL params use brace syntax: {projectId}, {id}, {taskId}, {labelId}, {assignmentId}

T11: Handler Tests (task-011)

  • Scope: Write HTTP integration tests for all handlers using httptest, chi router, and mock repositories. Follow the existing example_test.go handler test pattern.
  • Files:
    • services/studio-api/internal/api/handlers/project_test.go (new)
    • services/studio-api/internal/api/handlers/task_test.go (new)
    • services/studio-api/internal/api/handlers/label_test.go (new)
    • services/studio-api/internal/api/handlers/association_test.go (new)
  • Depends on: T9, T10
  • Acceptance criteria:
    • Each test file creates handler with mock repos via newTestXxxHandler() helper
    • Project handler tests: list, get (found/not found/invalid UUID), create (success/validation/duplicate), update (success/conflict/not found), delete (success/not found)
    • Task handler tests: list (with/without filters), get, create (success/project not found/validation), update, delete
    • Label handler tests: list, get, create (success/duplicate), update, delete
    • Association handler tests: attach label (success/duplicate/404), detach, list labels, assign user (success/duplicate/404), unassign, list assignments
    • All tests verify JSON response structure (data envelope)
    • All tests verify correct HTTP status codes
    • Tests pass with cd services/studio-api && go test -v ./internal/api/handlers/...

T12: Routes, OpenAPI Spec, and Main.go Wiring (task-012)

  • Scope: Register all new routes in routes.go, add OpenAPI schemas/paths in spec.go, and update main.go to connect to PostgreSQL, run migrations, create adapters, inject services, and register shutdown hooks.
  • Files:
    • services/studio-api/internal/api/routes.go (modify)
    • services/studio-api/internal/api/spec.go (modify)
    • services/studio-api/cmd/server/main.go (modify)
  • Depends on: T1, T5, T9, T10
  • Acceptance criteria:
    • routes.go: RegisterRoutes accepts all four services; registers Project, Task, Label, Association handlers
    • Route groups: public GET routes without auth, protected POST/PUT/DELETE routes with optional auth middleware
    • All routes under /api/studio-api/ prefix with {param} brace syntax
    • spec.go: OpenAPI schemas for Project, Task, Label, Assignment, TaskLabel, and all request types
    • spec.go: All 20+ endpoint paths documented with request/response schemas, auth requirements, and error responses
    • main.go: database.MustConnect() with cfg.Database.URL; database.MustRunMigrations() with embedded FS
    • main.go: Creates postgres adapters → services → passes to RegisterRoutes
    • main.go: pool.Close() registered via app.OnShutdown()
    • Existing Example entity routes and wiring remain untouched
    • All tests pass with cd services/studio-api && go test -v ./...