# 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 ./...`