build: /breakdown-feature data-models
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending

This commit is contained in:
rdev-worker 2026-02-11 05:11:10 +00:00
parent 021ae0af36
commit b4045b10ac
2 changed files with 291 additions and 1 deletions

View File

@ -32,5 +32,43 @@ artifacts:
approved_by: user
approved_at: 2026-02-11T05:00:33.399672269Z
tasks:
status: pending
status: draft
path: tasks.md
total: 12
tasks:
- id: task-001
title: SQL migrations - Create PostgreSQL migration files for projects, tasks, labels, task_labels, and assignments tables
status: pending
- id: task-002
title: Domain entities and errors - Create Project, Task, Label, Assignment, TaskLabel domain types with strong-typed IDs, constructors, validation, and domain error sentinels
status: pending
- id: task-003
title: Domain entity tests - Unit tests for all domain constructors, validation, and enum types
status: pending
- id: task-004
title: Port interfaces - Define repository interfaces for Project, Task, Label, TaskLabel, and Assignment
status: pending
- id: task-005
title: PostgreSQL adapters - Implement repository interfaces with sqlx for all five entities
status: pending
- id: task-006
title: Project and Label services - Business logic services for Project CRUD and Label CRUD with duplicate detection
status: pending
- id: task-007
title: Task and Association services - Business logic for Task CRUD with project FK validation, and AssociationService for task-label and task-user operations
status: pending
- id: task-008
title: Service layer tests - Unit tests for Project, Task, Label, and Association services with mock repositories
status: pending
- id: task-009
title: Project and Label HTTP handlers - CRUD handlers for projects and labels with request/response types and error mapping
status: pending
- id: task-010
title: Task and Association HTTP handlers - CRUD handlers for tasks and association endpoints (task-labels, assignments)
status: pending
- id: task-011
title: Handler tests - HTTP integration tests for all project, task, label, and association handlers
status: pending
- id: task-012
title: Routes, OpenAPI spec, and main.go wiring - Register all routes, update OpenAPI spec, wire database connection, migrations, and dependency injection in main.go
status: pending

View File

@ -0,0 +1,252 @@
# 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 ./...`