# Feature: Core Data Models & Persistence ## Problem Statement The studio application currently uses an in-memory storage adapter with a single example entity. To build real product functionality, the system needs persistent domain entities — Projects, Tasks, Labels, and Assignments — backed by PostgreSQL. Without these core data models, no meaningful product workflows (task management, project organization, label categorization, team assignment) can be built. ## User Stories - As a **developer**, I want Project, Task, Label, and Assignment entities with full CRUD so that the studio application has a foundation for product features. - As a **developer**, I want PostgreSQL-backed persistence via `studio-db` so that data survives service restarts. - As a **developer**, I want a repository layer with clean interfaces so that I can swap storage implementations and write testable code. - As a **developer**, I want service-layer business logic so that domain rules (validation, uniqueness, referential integrity) are enforced consistently. - As a **developer**, I want REST endpoints on `studio-api` so that frontends and external clients can manage these entities. - As a **developer**, I want database migrations so that schema changes are versioned, repeatable, and safe to apply. - As a **developer**, I want handler tests so that API behavior is verified and regressions are caught. ## Domain Model ### Project | Field | Type | Constraints | |-------|------|-------------| | `id` | UUID | PK, generated | | `name` | string | required, 1–200 chars, unique | | `description` | string | optional, max 1000 chars | | `status` | enum | `active`, `archived`; default `active` | | `created_at` | timestamp | UTC, set on create | | `updated_at` | timestamp | UTC, set on create/update | ### Task | Field | Type | Constraints | |-------|------|-------------| | `id` | UUID | PK, generated | | `project_id` | UUID | FK → projects, required | | `title` | string | required, 1–300 chars | | `description` | string | optional, max 5000 chars | | `status` | enum | `todo`, `in_progress`, `done`; default `todo` | | `priority` | enum | `low`, `medium`, `high`; default `medium` | | `position` | integer | ordering within project, default 0 | | `created_at` | timestamp | UTC | | `updated_at` | timestamp | UTC | ### Label | Field | Type | Constraints | |-------|------|-------------| | `id` | UUID | PK, generated | | `project_id` | UUID | FK → projects, required | | `name` | string | required, 1–50 chars, unique per project | | `color` | string | required, hex color (e.g., `#FF5733`) | | `created_at` | timestamp | UTC | | `updated_at` | timestamp | UTC | ### Assignment (join entity: Task ↔ User) | Field | Type | Constraints | |-------|------|-------------| | `id` | UUID | PK, generated | | `task_id` | UUID | FK → tasks, required | | `assignee` | string | required, 1–200 chars (user identifier) | | `created_at` | timestamp | UTC | **Note:** `assignee` is a string identifier (email or user ID) rather than a FK to a users table, since user management is out of scope for this feature. A unique constraint on `(task_id, assignee)` prevents duplicate assignments. ## Acceptance Criteria ### Migrations - [ ] Migration `001_create_projects.sql` creates the `projects` table with all fields, constraints, and indexes - [ ] Migration `002_create_tasks.sql` creates the `tasks` table with FK to projects, indexes on `project_id` and `status` - [ ] Migration `003_create_labels.sql` creates the `labels` table with FK to projects, unique constraint on `(project_id, name)` - [ ] Migration `004_create_assignments.sql` creates the `assignments` table with FK to tasks, unique constraint on `(task_id, assignee)` - [ ] Migrations run via the existing `database.MustRunMigrations()` system with embedded SQL files - [ ] All migrations are idempotent and run in transactions ### Domain Layer - [ ] Domain entities `Project`, `Task`, `Label`, `Assignment` defined with strongly-typed IDs (e.g., `ProjectID`, `TaskID`) - [ ] Domain constructors (`NewProject`, `NewTask`, `NewLabel`, `NewAssignment`) validate all required fields - [ ] Domain errors defined: `ErrProjectNotFound`, `ErrTaskNotFound`, `ErrLabelNotFound`, `ErrAssignmentNotFound`, `ErrDuplicateProjectName`, `ErrDuplicateLabelName`, `ErrDuplicateAssignment`, `ErrInvalidStatus`, `ErrInvalidPriority` - [ ] Update methods validate mutable fields and set `updated_at` ### Repository Layer (Ports) - [ ] `ProjectRepository` interface with `List`, `Get`, `Create`, `Update`, `Delete`, `ExistsByName` - [ ] `TaskRepository` interface with `ListByProject`, `Get`, `Create`, `Update`, `Delete` - [ ] `LabelRepository` interface with `ListByProject`, `Get`, `Create`, `Update`, `Delete`, `ExistsByName` - [ ] `AssignmentRepository` interface with `ListByTask`, `Get`, `Create`, `Delete`, `ExistsByTaskAndAssignee` ### Adapter Layer (Postgres) - [ ] `adapter/postgres/` package implementing all repository interfaces using `sqlx` - [ ] Proper SQL parameterization (`$1`, `$2`, etc.) — no string concatenation - [ ] `sql.ErrNoRows` mapped to domain `ErrNotFound` errors - [ ] Unique constraint violations mapped to domain duplicate errors ### Service Layer - [ ] `ProjectService` with Create (duplicate name check), Get, List, Update, Delete (cascade consideration) - [ ] `TaskService` with Create (validate project exists), Get, ListByProject, Update, Delete - [ ] `LabelService` with Create (validate project exists, duplicate name per project), Get, ListByProject, Update, Delete - [ ] `AssignmentService` with Create (validate task exists, duplicate check), ListByTask, Delete ### Handler Layer (REST API) - [ ] **Projects:** `POST /api/studio-api/projects`, `GET /api/studio-api/projects`, `GET /api/studio-api/projects/{id}`, `PUT /api/studio-api/projects/{id}`, `DELETE /api/studio-api/projects/{id}` - [ ] **Tasks:** `POST /api/studio-api/projects/{projectId}/tasks`, `GET /api/studio-api/projects/{projectId}/tasks`, `GET /api/studio-api/tasks/{id}`, `PUT /api/studio-api/tasks/{id}`, `DELETE /api/studio-api/tasks/{id}` - [ ] **Labels:** `POST /api/studio-api/projects/{projectId}/labels`, `GET /api/studio-api/projects/{projectId}/labels`, `GET /api/studio-api/labels/{id}`, `PUT /api/studio-api/labels/{id}`, `DELETE /api/studio-api/labels/{id}` - [ ] **Assignments:** `POST /api/studio-api/tasks/{taskId}/assignments`, `GET /api/studio-api/tasks/{taskId}/assignments`, `DELETE /api/studio-api/assignments/{id}` - [ ] All handlers follow `app.Wrap()` + `app.BindAndValidate()` pattern - [ ] All responses use `httpresponse.OK`, `httpresponse.Created`, `httpresponse.NoContent` envelope - [ ] URL parameters use `{param}` brace syntax, extracted with `chi.URLParam()` - [ ] Domain errors mapped to HTTP errors via `mapDomainError()` functions ### OpenAPI Specification - [ ] All schemas defined in `spec.go` using `openapi.*` helpers - [ ] All endpoints documented with request/response types, status codes, and tags - [ ] Tags: `Projects`, `Tasks`, `Labels`, `Assignments` ### Tests - [ ] Handler tests for all CRUD operations on each entity using mock repositories - [ ] Table-driven tests covering: success, not found, validation failure, duplicate/conflict - [ ] Test setup uses `newTestHandler()` pattern with mock repositories - [ ] Tests use `chi.NewRouter()` + `httptest` for HTTP testing ### Wiring - [ ] `main.go` updated to: connect to database, run migrations, create postgres adapters, inject into services and handlers - [ ] Database connection uses `config.ReadDatabaseConfig()` + `database.MustConnect()` - [ ] Migrations embedded with `//go:embed migrations/*.sql` - [ ] Graceful shutdown closes database pool ## Technical Constraints - **Database:** PostgreSQL via `sqlx` + `lib/pq` driver (already in `pkg/database`) - **Migration system:** Embedded SQL files using `database.MustRunMigrations()` — no external migration tool - **Architecture:** Hexagonal architecture — domain has no external dependencies; ports define interfaces; adapters implement them - **Routing:** chi router with `{param}` brace syntax; all handlers return `error` wrapped with `app.Wrap()` - **Validation:** Struct tags using `go-playground/validator` via `app.BindAndValidate()` - **ID generation:** UUIDs generated server-side (e.g., `uuid.New().String()`) - **Timestamps:** All UTC, set by domain constructors - **Cascading deletes:** Deleting a project should delete its tasks, labels, and associated assignments (via `ON DELETE CASCADE` in FK constraints) ## Dependencies - `pkg/database` — PostgreSQL connection pool and migration runner (exists) - `pkg/config` — Database URL and pool configuration (exists) - `pkg/app` — Handler wrapping, binding, routing (exists) - `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` — HTTP layer helpers (exists) - `pkg/openapi` — API documentation (exists) - Running PostgreSQL instance for local development and integration tests - `github.com/google/uuid` — UUID generation (add to go.mod if not present) ## Out of Scope - **User management / authentication:** `assignee` is a plain string, not a FK to a users table. Auth middleware integration is not part of this feature. - **Pagination:** List endpoints return all records. Pagination will be a follow-up feature. - **Filtering/sorting:** List endpoints are unfiltered. Query parameters for filtering by status, priority, etc. will be a follow-up. - **Task-Label association:** Many-to-many relationship between tasks and labels (a `task_labels` join table) is not included. This is a follow-up feature. - **Soft deletes:** All deletes are hard deletes. Soft delete (archive) behavior may be added later. - **Audit logging:** No change tracking or audit trail in this feature. - **Frontend integration:** No UI changes. This is backend-only. ## Open Questions 1. **Delete cascade vs. restrict:** Should deleting a project cascade-delete all tasks, labels, and assignments? The spec assumes `ON DELETE CASCADE`, but an alternative is to return an error if a project has tasks (forcing explicit cleanup). Which behavior is preferred? 2. **Assignment entity vs. simple field:** Should task assignment be a separate entity with its own table (supporting multiple assignees per task), or a simple `assignee` string field on the Task table (single assignee)? The spec assumes a separate join entity for multiple assignees. 3. **Task position/ordering:** The spec includes a `position` integer for ordering tasks within a project. Should this be included in the initial implementation, or deferred until a drag-and-drop UI feature is built? 4. **Project status transitions:** Should there be business rules governing status transitions (e.g., can only archive a project if all tasks are `done`)? The spec currently allows free status changes. 5. **Database name:** The requirements mention `studio-db` as the database. Should this be a new PostgreSQL database name, or should it refer to the database configured in `DATABASE_URL`? The spec assumes the existing `DATABASE_URL` configuration.