foundary-test-1770784989/.sdlc/features/data-models/spec.md
rdev-worker 9c9e07706f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature data-models --requirements 'Define Task, Project, Label...
2026-02-11 05:00:21 +00:00

8.0 KiB

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.