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

11 KiB
Raw Blame History

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.