foundary-test-1770773605/.sdlc/features/data-models/spec.md
rdev-worker badb4c084c
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature data-models --requirements 'Define Task, Project, Label...
2026-02-11 01:53:16 +00:00

169 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, 1200 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, 1300 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, 150 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, 1200 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.