# Feature: Core Data Models & Persistence ## Problem Statement The studio-api service currently uses in-memory storage (via `adapter/memory`) with a single `Example` entity. To support real product development workflows, the service needs persistent, relational data models for managing **Projects**, **Tasks**, **Labels**, and **Assignments**. Without these, the application cannot track work items, organize them into projects, categorize with labels, or assign work to users. These entities form the backbone of a project management domain and must be stored in PostgreSQL via the existing `pkg/database` infrastructure. ## User Stories - As a **user**, I want to create and manage **projects** so that I can organize related work items together. - As a **user**, I want to create, update, and delete **tasks** within a project so that I can track individual work items. - As a **user**, I want to create and manage **labels** so that I can categorize and filter tasks. - As a **user**, I want to attach labels to tasks so that tasks can be categorized across multiple dimensions. - As a **user**, I want to assign tasks to users so that ownership and responsibility are clear. - As a **user**, I want to list tasks with filtering (by project, label, assignee, status) so that I can find relevant work items quickly. ## Acceptance Criteria ### Projects - [ ] Project entity with fields: id (UUID), name, description, created_at, updated_at - [ ] Full CRUD endpoints: `POST /projects`, `GET /projects`, `GET /projects/{id}`, `PUT /projects/{id}`, `DELETE /projects/{id}` - [ ] Project name is required (1-100 chars), description optional (max 500 chars) - [ ] Deleting a project cascades to its tasks, task-label associations, and assignments - [ ] Duplicate project names return 409 Conflict ### Tasks - [ ] Task entity with fields: id (UUID), project_id (FK), title, description, status, priority, created_at, updated_at - [ ] Status enum: `open`, `in_progress`, `done`, `cancelled` - [ ] Priority enum: `low`, `medium`, `high`, `critical` - [ ] Full CRUD endpoints: `POST /projects/{projectId}/tasks`, `GET /projects/{projectId}/tasks`, `GET /tasks/{id}`, `PUT /tasks/{id}`, `DELETE /tasks/{id}` - [ ] Task title is required (1-200 chars), description optional (max 2000 chars) - [ ] Tasks belong to exactly one project (project_id required, validated) - [ ] Creating a task for a non-existent project returns 404 - [ ] List tasks supports filtering by status and priority via query parameters ### Labels - [ ] Label entity with fields: id (UUID), name, color, created_at - [ ] Full CRUD endpoints: `POST /labels`, `GET /labels`, `GET /labels/{id}`, `PUT /labels/{id}`, `DELETE /labels/{id}` - [ ] Label name is required (1-50 chars), unique; color is optional (hex format, e.g. `#FF5733`) - [ ] Deleting a label removes its task-label associations (cascade) - [ ] Duplicate label names return 409 Conflict ### Assignments (Task-Label & Task-User associations) - [ ] Task-Label join: `POST /tasks/{taskId}/labels/{labelId}`, `DELETE /tasks/{taskId}/labels/{labelId}`, `GET /tasks/{taskId}/labels` - [ ] Task-User assignment: `POST /tasks/{taskId}/assignments`, `DELETE /tasks/{taskId}/assignments/{assignmentId}`, `GET /tasks/{taskId}/assignments` - [ ] Assignment entity with fields: id (UUID), task_id (FK), user_id (string), assigned_at - [ ] Assigning a non-existent label or task returns 404 - [ ] Duplicate task-label associations return 409 Conflict - [ ] Duplicate task-user assignments return 409 Conflict ### Database & Migrations - [ ] PostgreSQL migrations in `services/studio-api/migrations/` using `pkg/database.RunMigrations` - [ ] Migration files follow `NNN_description.sql` naming convention - [ ] All tables use UUID primary keys - [ ] Foreign keys with appropriate ON DELETE CASCADE constraints - [ ] Indexes on foreign key columns and commonly queried fields (status, priority) - [ ] Schema supports the `sqlx` struct scanning pattern (snake_case column names) ### Architecture & Code Quality - [ ] Domain entities in `internal/domain/` with constructor validation (follows `Example` pattern) - [ ] Strong-typed IDs (`ProjectID`, `TaskID`, `LabelID`, `AssignmentID`) following `ExampleID` pattern - [ ] Domain errors in `internal/domain/errors.go` for each entity (NotFound, Duplicate, Invalid) - [ ] Port interfaces in `internal/port/` defining repository contracts - [ ] PostgreSQL adapter implementations in `internal/adapter/postgres/` - [ ] Service layer in `internal/service/` with business logic and validation - [ ] HTTP handlers in `internal/api/handlers/` following `app.Wrap()` pattern - [ ] Routes registered in `internal/api/routes.go` under `/api/studio-api/` - [ ] OpenAPI spec updated in `internal/api/spec.go` - [ ] `main.go` updated to wire PostgreSQL pool and new services ### Testing - [ ] Unit tests for each service layer (mock repositories, follows `example_test.go` pattern) - [ ] Unit tests for each handler (httptest, chi router, follows `example_test.go` pattern) - [ ] Domain entity constructor and validation tests - [ ] All tests pass with `cd services/studio-api && go test -v ./...` ### Response Format - [ ] All endpoints use `httpresponse.OK/Created/NoContent` envelope pattern - [ ] All errors use `httperror.BadRequest/NotFound/Conflict` typed errors - [ ] Request bodies validated with `app.BindAndValidate()` - [ ] URL parameters use `{param}` brace syntax (not colon syntax) ## Technical Constraints - **Database**: PostgreSQL via `pkg/database` (sqlx-based, `*sqlx.DB`) - **Migrations**: Embedded SQL files via `//go:embed`, run at startup with `database.RunMigrations` - **IDs**: UUID v4, generated server-side (use `github.com/google/uuid`) - **JSON serialization**: Standard `encoding/json` with `json:"field_name"` tags - **Validation**: `app.BindAndValidate()` for request bodies, domain-level validation for business rules - **Router**: Chi with `{param}` brace syntax for URL parameters - **No ORM**: Use raw SQL with sqlx (`Get`, `Select`, `NamedExec`, `ExecContext`) - **Transactions**: Use `database.WithTx` for multi-table mutations (e.g., cascade deletes if not handled by DB constraints) ## Dependencies - `pkg/database` - PostgreSQL connection pool and migration runner (exists) - `pkg/app` - Service bootstrapper, `Wrap`, `Bind`, `BindAndValidate` (exists) - `pkg/httperror` - Typed HTTP errors (exists) - `pkg/httpresponse` - Response envelope helpers (exists) - `pkg/logging` - Structured logging (exists) - `pkg/auth` - JWT middleware for protected routes (exists) - `pkg/openapi` - OpenAPI spec builder (exists) - `github.com/google/uuid` - UUID generation (add to go.mod) - PostgreSQL instance accessible via `DATABASE_URL` environment variable ## Out of Scope - User management / user CRUD (user_id in assignments is an opaque string, not a managed entity) - File attachments on tasks - Task comments or activity history - Real-time notifications (WebSocket updates for task changes) - Pagination (can be added later; initial list endpoints return all matching records) - Full-text search on task titles/descriptions - Soft deletes (hard deletes for simplicity; soft deletes can be added later) - Removing the existing `Example` entity and its in-memory adapter (keep for reference) ## Open Questions 1. **User identity format**: Should `user_id` in assignments be a UUID (anticipating a future users table) or a free-form string (e.g., email or external auth ID)? *Spec assumes string for flexibility.* 2. **Task ordering**: Should tasks have an explicit `position`/`sort_order` field for manual reordering within a project, or is ordering by `created_at` sufficient for v1? 3. **Label scoping**: Should labels be global (shared across all projects) or scoped per-project? *Spec assumes global labels.* 4. **Cascade behavior**: When a project is deleted, should its tasks be deleted (cascade) or should deletion be blocked if tasks exist? *Spec assumes cascade delete via FK constraint.* 5. **Auth on reads**: Should `GET` (list/detail) endpoints be public or require authentication? *Current `Example` pattern has reads as public, writes as protected. Spec follows this pattern.*