build: /spec-feature data-models --requirements 'Define Task, Project, Label...
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
rdev-worker 2026-02-11 01:53:16 +00:00
parent 265a2e5b90
commit badb4c084c
2 changed files with 169 additions and 1 deletions

View File

@ -22,7 +22,7 @@ artifacts:
status: pending
path: review.md
spec:
status: pending
status: draft
path: spec.md
tasks:
status: pending

View File

@ -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, 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.