11 KiB
11 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Domain layer - preferences types, validation, defaults, and errors
- Scope: Create the pure domain model for user preferences. Define
UserID,NotificationPreferences,Preferences(with custom JSON marshal/unmarshal for unknown key preservation viaExtramap), andUserPreferencestypes. ImplementDefaultPreferences()as the single source of truth for defaults. ImplementPreferences.Validate()with theme enum and language length checks. Add domain errors:ErrInvalidTheme,ErrInvalidLanguage,ErrForbidden,ErrPreferencesNotFound. - Files:
- Create:
services/preferences-api/internal/domain/preferences.go - Modify:
services/preferences-api/internal/domain/errors.go
- Create:
- Depends on: None
- Acceptance criteria:
UserPreferences,Preferences,NotificationPreferences,UserIDtypes definedDefaultPreferences()returns theme="system", language="en", notifications email=true, push=true, sms=falseValidate()rejects theme values not in["light", "dark", "system"]withErrInvalidThemeValidate()allows empty theme (treated as valid, will use default)Validate()rejects language longer than 10 runes withErrInvalidLanguage- Custom
MarshalJSON/UnmarshalJSONonPreferencespreserves unknown keys viaExtramap ErrInvalidTheme,ErrInvalidLanguage,ErrForbidden,ErrPreferencesNotFounddefined in errors.go- No external dependencies (pure domain)
T2: Port layer - PreferencesRepository interface
- Scope: Define the
PreferencesRepositoryinterface withGetandUpsertmethods. This is the contract between the service layer and the persistence adapter. - Files:
- Create:
services/preferences-api/internal/port/preferences.go
- Create:
- Depends on: T1
- Acceptance criteria:
PreferencesRepositoryinterface defined withGet(ctx, userID) (*domain.UserPreferences, error)andUpsert(ctx, prefs *domain.UserPreferences) error- Uses
domain.UserIDanddomain.UserPreferencestypes from T1 - No implementation, interface only
T3: Database migration - user_preferences table
- Scope: Create the SQL migration file for the
user_preferencestable withuser_id TEXT PRIMARY KEY,preferences JSONB NOT NULL,created_at TIMESTAMPTZ, andupdated_at TIMESTAMPTZ. Set up the//go:embedmigration embedding in a migrations package. - Files:
- Create:
services/preferences-api/migrations/001_create_user_preferences.sql - Create or modify:
services/preferences-api/migrations/migrations.go(embed directive)
- Create:
- Depends on: None
- Acceptance criteria:
- Migration creates
user_preferencestable withIF NOT EXISTS user_id TEXT PRIMARY KEYcolumnpreferences JSONB NOT NULLcolumncreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()columnupdated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()column- Migration file embedded via
//go:embedper project convention
- Migration creates
T4: PostgreSQL adapter - PreferencesRepository implementation
- Scope: Implement the
postgres.PreferencesRepositorystruct that satisfies theport.PreferencesRepositoryinterface.Getqueries byuser_idand returnsdomain.ErrPreferencesNotFoundwhen no row exists.UpsertusesINSERT ... ON CONFLICT (user_id) DO UPDATE SETfor idempotent writes. JSON marshaling betweendomain.Preferencesand the JSONB column. - Files:
- Create:
services/preferences-api/internal/adapter/postgres/preferences.go
- Create:
- Depends on: T1, T2, T3
- Acceptance criteria:
postgres.PreferencesRepositorystruct with*sqlx.DBand loggerNewPreferencesRepository(db, logger)constructorGetreturnsdomain.ErrPreferencesNotFoundwhen no row found (sql.ErrNoRows)Getunmarshals JSONB intodomain.PreferencescorrectlyUpsertuses parameterizedINSERT ... ON CONFLICT DO UPDATEqueryUpsertmarshalsdomain.Preferencesto JSON for JSONB storage- All queries use parameterized statements (no SQL injection)
T5: Service layer - PreferencesService with get/set logic and tests
- Scope: Implement
PreferencesServicewithGetPreferencesandSetPreferencesmethods.GetPreferencesreturns defaults when the repo returnsErrPreferencesNotFound.SetPreferencesvalidates via domain, builds the entity, and upserts. Write comprehensive unit tests with a mock repository. - Files:
- Create:
services/preferences-api/internal/service/preferences.go - Create:
services/preferences-api/internal/service/preferences_test.go
- Create:
- Depends on: T1, T2
- Acceptance criteria:
PreferencesServicestruct withport.PreferencesRepositoryand loggerNewPreferencesService(repo, logger)constructorGetPreferencesreturns stored preferences when they existGetPreferencesreturnsDefaultPreferences()with givenuserIDwhen repo returnsErrPreferencesNotFoundSetPreferencescallsValidate()on input and returns domain errors for invalid dataSetPreferencescallsrepo.Upsert()and returns the saved entity- Unit tests use a mock
PreferencesRepository - Tests cover: get with existing prefs, get with defaults, set valid prefs, set with invalid theme, set with invalid language
- Tests use
logging.Nop()for logger
T6: HTTP handlers - GET and PUT with auth, mapping, and tests
- Scope: Implement the
Preferenceshandler struct withGetandPutmethods. Both extractuser_idfrom URL params, perform authorization check (authenticated user matches pathuser_id), and delegate to the service. Request/response DTO types for JSON serialization.mapDomainError()function to convert domain errors tohttperror.*. Handler unit tests with mocked service. - Files:
- Create:
services/preferences-api/internal/api/handlers/preferences.go - Create:
services/preferences-api/internal/api/handlers/preferences_test.go
- Create:
- Depends on: T1, T5
- Acceptance criteria:
Preferenceshandler struct with service and loggerNewPreferences(svc, logger)constructorGethandler: extractsuser_idviachi.URLParam, checks auth viaauth.GetUser, returns 403 if user mismatch, returns preferences viahttpresponse.OKPuthandler: extractsuser_id, checks auth, binds request viaapp.BindAndValidate, delegates to service, returns saved preferences viahttpresponse.OK- Request DTOs:
PutPreferencesRequest,PreferencesPayload,NotificationPreferencesPayload - Response DTO:
PreferencesResponsewithuser_id,preferences,updated_at mapDomainError()mapsErrInvalidThemeandErrInvalidLanguagetohttperror.BadRequesttoResponse()andtoDomain()conversion functions- Tests cover: GET success, GET defaults, GET forbidden, PUT success, PUT validation error, PUT forbidden
T7: Routes, OpenAPI spec, and main.go wiring
- Scope: Update
routes.goto register authenticated preference routes (GET /{user_id},PUT /{user_id}) under/api/preferences-api/preferenceswithauth.Middleware(). Updatespec.gowith preference schemas and endpoint documentation. Updatemain.goto connect to PostgreSQL viadatabase.Connect(), run migrations, create the Postgres adapter, wire the service, and register cleanup on shutdown. - Files:
- Modify:
services/preferences-api/internal/api/routes.go - Modify:
services/preferences-api/internal/api/spec.go - Modify:
services/preferences-api/cmd/server/main.go
- Modify:
- Depends on: T3, T4, T5, T6
- Acceptance criteria:
- Routes registered:
GET /api/preferences-api/preferences/{user_id}andPUT /api/preferences-api/preferences/{user_id} - Auth middleware applied to preference route group (conditional on
AuthEnabled) - URL parameters use brace syntax
{user_id} - OpenAPI spec defines
Preferences,NotificationPreferences,UserPreferencesResponse,PutPreferencesRequestschemas - OpenAPI spec documents both endpoints with auth requirements and error responses
main.goconnects to PostgreSQL withdatabase.Connect()main.goruns migrations viadatabase.MustRunMigrations()main.gocreates postgres repo, preferences service, and passes to route registrationmain.goregisterspool.Close()for graceful shutdown- Health check route preserved at
/api/preferences-api/health
- Routes registered:
T8: Remove example scaffold code
- Scope: Delete all example/scaffold files that are replaced by the preferences implementation. This is the final cleanup task.
- Files:
- Delete:
services/preferences-api/internal/domain/example.go - Delete:
services/preferences-api/internal/port/example.go - Delete:
services/preferences-api/internal/service/example.go - Delete:
services/preferences-api/internal/service/example_test.go - Delete:
services/preferences-api/internal/adapter/memory/example.go - Delete:
services/preferences-api/internal/api/handlers/example.go - Delete:
services/preferences-api/internal/api/handlers/example_test.go
- Delete:
- Depends on: T7 (all new code is in place before deleting old code)
- Acceptance criteria:
- All 7 example files deleted
internal/adapter/memory/directory removed (no longer needed)- No remaining references to
Example,ExampleService,ExampleRepositoryin the codebase - Service compiles cleanly (
go build ./...) - All tests pass (
go test ./...)
Dependency Graph
T1 (Domain) ──┬──▶ T2 (Port) ──┬──▶ T5 (Service) ──┬──▶ T6 (Handlers) ──┐
│ │ │ │
│ └──▶ T4 (Adapter) ──┐│ │
│ ││ │
T3 (Migration)─────────────────────▶ T4 ───────────┘│ │
│ │
└──▶ T7 (Wiring) ───┤
│
T8 (Cleanup) ◀──┘
Longest dependency chain: T1 → T2 → T5 → T6 → T7 → T8 (depth: 6)
Parallelization opportunities:
- T1 and T3 can be done in parallel (no dependencies)
- T2 can start immediately after T1
- T4 can start after T1 + T2 + T3
- T5 can start after T1 + T2 (doesn't need T3 or T4)