8.0 KiB
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/usingpkg/database.RunMigrations - Migration files follow
NNN_description.sqlnaming 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
sqlxstruct scanning pattern (snake_case column names)
Architecture & Code Quality
- Domain entities in
internal/domain/with constructor validation (followsExamplepattern) - Strong-typed IDs (
ProjectID,TaskID,LabelID,AssignmentID) followingExampleIDpattern - Domain errors in
internal/domain/errors.gofor 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/followingapp.Wrap()pattern - Routes registered in
internal/api/routes.gounder/api/studio-api/ - OpenAPI spec updated in
internal/api/spec.go main.goupdated to wire PostgreSQL pool and new services
Testing
- Unit tests for each service layer (mock repositories, follows
example_test.gopattern) - Unit tests for each handler (httptest, chi router, follows
example_test.gopattern) - 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/NoContentenvelope pattern - All errors use
httperror.BadRequest/NotFound/Conflicttyped 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 withdatabase.RunMigrations - IDs: UUID v4, generated server-side (use
github.com/google/uuid) - JSON serialization: Standard
encoding/jsonwithjson:"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.WithTxfor 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_URLenvironment 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
Exampleentity and its in-memory adapter (keep for reference)
Open Questions
- User identity format: Should
user_idin 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. - Task ordering: Should tasks have an explicit
position/sort_orderfield for manual reordering within a project, or is ordering bycreated_atsufficient for v1? - Label scoping: Should labels be global (shared across all projects) or scoped per-project? Spec assumes global labels.
- 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.
- Auth on reads: Should
GET(list/detail) endpoints be public or require authentication? CurrentExamplepattern has reads as public, writes as protected. Spec follows this pattern.