17 KiB
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.sqlcreatesprojectstable with UUID PK, name (VARCHAR 100, UNIQUE, NOT NULL), description (VARCHAR 500), created_at/updated_at (TIMESTAMPTZ with defaults)002_create_tasks.sqlcreatestaskstable with UUID PK, project_id FK (CASCADE), title, description, status, priority, timestamps; indexes on project_id, status, priority003_create_labels_and_associations.sqlcreateslabels,task_labels(composite PK), andassignmentstables 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:
ProjectIDstrong type;NewProject(name, description)validates name (1-100 chars), description (max 500);Update(name, description)methodTaskID,TaskStatus,TaskPrioritystrong types with const enums;NewTask(projectID, title, description, status, priority)validates all fields;Update(...)method; defaults status=open, priority=medium when emptyLabelIDstrong type;NewLabel(name, color)validates name (1-50 chars), color (optional, hex format^#[0-9A-Fa-f]{6}$);Update(name, color)methodAssignmentIDstrong type;NewAssignment(taskID, userID)constructorTaskLabelstruct withNewTaskLabel(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.ContextTaskRepository: ListByProject (with optional status/priority filters), Get, Create, Update, Delete methodsLabelRepository: List, Get, Create, Update, Delete, ExistsByName methodsTaskLabelRepository: ListByTask, Attach, Detach, Exists methodsAssignmentRepository: ListByTask, Get, Create, Delete, ExistsByTaskAndUser methods- All methods accept
context.Contextas 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.DBand has aNew*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_idsql.ErrNoRowsmapped 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)
- Each adapter struct takes
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:
ProjectServicewith List, Get, Create, Update, Delete methodsProjectService.Createchecks ExistsByName before creating; returns ErrDuplicateProject on conflictProjectService.Updatechecks name conflicts (excluding self); returns ErrDuplicateProject on conflictLabelServicewith List, Get, Create, Update, Delete methodsLabelService.Createchecks ExistsByName before creating; returns ErrDuplicateLabel on conflictLabelService.Updatechecks name conflicts (excluding self); returns ErrDuplicateLabel on conflict- Both services use
CreateInput/UpdateInputDTOs (not domain types directly) - Both services accept
port.XxxRepositoryand*slog.Loggervia 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:
TaskServicewith List (by project, filterable), Get, Create, Update, Delete methodsTaskService.Createvalidates that the project exists (via ProjectRepository.Get) before creating; returns ErrProjectNotFound if notTaskService.Listaccepts optional status and priority filter parametersAssociationServicewith AttachLabel, DetachLabel, ListLabelsForTask, AssignUser, UnassignUser, ListAssignmentsForTask methodsAssociationService.AttachLabelverifies task and label exist before attaching; returns 404 if either missing, 409 if duplicateAssociationService.AssignUserverifies 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 CreateProjectRequestwithName(required, min=1, max=100) andDescription(max=500) validation tagsUpdateProjectRequestwith same validationProjectResponsestruct with JSON tags;toProjectResponse()mappermapProjectDomainError()maps ErrProjectNotFound→404, ErrDuplicateProject→409, ErrInvalidProjectName→400- Label handler: List, Get, Create, Update, Delete methods
CreateLabelRequestwithName(required, min=1, max=50) andColor(optional) validation tagsmapLabelDomainError()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.OKfor GET/PUT,httpresponse.Createdfor POST,httpresponse.NoContentfor DELETE
- Project handler: List, Get, Create, Update, Delete methods all returning
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
projectIdfrom URL + query params for status/priority), Get, Create (readsprojectIdfrom URL), Update, Delete CreateTaskRequestwithTitle(required, min=1, max=200),Description(max=2000),Status(oneof enum),Priority(oneof enum) validationmapTaskDomainError()maps ErrTaskNotFound→404, ErrProjectNotFound→404, ErrInvalidTaskTitle→400, ErrInvalidTaskStatus→400, ErrInvalidTaskPriority→400- Association handler: AttachLabel, DetachLabel, ListLabelsForTask, AssignUser, UnassignUser, ListAssignmentsForTask
AssignUserRequestwithUserID(required) fieldmapAssociationDomainError()maps all association errors (404, 409)- Task list handler reads
?status=and?priority=query parameters viar.URL.Query() - All URL params use brace syntax:
{projectId},{id},{taskId},{labelId},{assignmentId}
- Task handler: List (reads
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/...
- Each test file creates handler with mock repos via
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 typesspec.go: All 20+ endpoint paths documented with request/response schemas, auth requirements, and error responsesmain.go:database.MustConnect()withcfg.Database.URL;database.MustRunMigrations()with embedded FSmain.go: Creates postgres adapters → services → passes to RegisterRoutesmain.go:pool.Close()registered viaapp.OnShutdown()- Existing Example entity routes and wiring remain untouched
- All tests pass with
cd services/studio-api && go test -v ./...