diff --git a/.sdlc/features/data-models/manifest.yaml b/.sdlc/features/data-models/manifest.yaml index efa1703..b82dd60 100644 --- a/.sdlc/features/data-models/manifest.yaml +++ b/.sdlc/features/data-models/manifest.yaml @@ -22,7 +22,7 @@ artifacts: status: pending path: review.md spec: - status: pending + status: draft path: spec.md tasks: status: pending diff --git a/.sdlc/features/data-models/spec.md b/.sdlc/features/data-models/spec.md new file mode 100644 index 0000000..ac5ac9b --- /dev/null +++ b/.sdlc/features/data-models/spec.md @@ -0,0 +1,168 @@ +# 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.