9.1 KiB
9.1 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Domain layer - preference types, validation, defaults, and domain errors
- Scope: Create the core domain model:
PreferenceKeyconstants,PreferenceDefinitionregistry with default values and per-key validators,UserPreferencesaggregate, helper functions (DefaultPreferences,ValidateKey,ValidateValue,MergeWithDefaults,SerializeForResponse), and domain errors (ErrUnknownPreferenceKey,ErrInvalidPreferenceValue,ErrInvalidUserID). Also implementValidateUserID(UUID format check). - Files:
- Create
services/preferences-api/internal/domain/preference.go - Modify
services/preferences-api/internal/domain/errors.go(replace example errors) - Delete
services/preferences-api/internal/domain/example.go
- Create
- Depends on: None
- Acceptance criteria:
PreferenceKeyconstants defined fortheme,language,notifications_enabledDefaultPreferences()returns all 3 keys with correct defaults (system,en,true)ValidateKey()rejects unknown keys, accepts known keysValidateValue()enforces: theme in {light, dark, system}, language matches BCP 47 regex, notifications_enabled in {true, false}MergeWithDefaults()fills missing keys with defaults, preserves stored valuesSerializeForResponse()converts"true"/"false"to boolean fornotifications_enabled- Domain errors are sentinel errors with descriptive messages
ValidateUserID()accepts valid UUIDs, rejects non-UUID strings
T2: Port layer - PreferenceRepository interface and row type
- Scope: Define the
PreferenceRepositoryport interface withGetByUserIDandUpsertmethods, plus thePreferenceRowdata transfer type. - Files:
- Create
services/preferences-api/internal/port/preference.go - Delete
services/preferences-api/internal/port/example.go
- Create
- Depends on: T1
- Acceptance criteria:
PreferenceRepositoryinterface defined withGetByUserID(ctx, userID) ([]PreferenceRow, error)andUpsert(ctx, userID, key, value) errorPreferenceRowstruct has fields:UserID,Key,Value,CreatedAt,UpdatedAt- Interface is minimal (no delete, no list-all-users)
T3: Service layer - PreferenceService with get, update, validation logic and unit tests
- Scope: Implement
PreferenceServicewithGetPreferencesandUpdatePreferencesmethods.GetPreferencesfetches stored rows, merges with defaults, serializes for response.UpdatePreferencesvalidates keys/values, upserts each, then returns full merged result. Write comprehensive unit tests with a mock repository. - Files:
- Create
services/preferences-api/internal/service/preference.go - Create
services/preferences-api/internal/service/preference_test.go - Delete
services/preferences-api/internal/service/example.go - Delete
services/preferences-api/internal/service/example_test.go
- Create
- Depends on: T1, T2
- Acceptance criteria:
GetPreferencesreturns all defaults when no rows storedGetPreferencesmerges stored values with defaults for missing keysGetPreferencesreturnsErrInvalidUserIDfor non-UUID user_idUpdatePreferencesrejects unknown keys withErrUnknownPreferenceKeyUpdatePreferencesrejects invalid values withErrInvalidPreferenceValueUpdatePreferencescallsUpsertfor each provided keyUpdatePreferencesreturns full merged preferences after upsertUpdatePreferenceshandles boolean input (convertstrue/falseto string)- Unit tests cover: defaults-only, partial stored, full stored, unknown key, invalid value, invalid user_id
- Tests use mock repository (no database dependency)
T4: Database migration - create user_preferences table
- Scope: Write the SQL migration to create the
user_preferencestable with composite primary key(user_id, key)and an index onuser_id. - Files:
- Create
services/preferences-api/migrations/001_create_user_preferences.sql
- Create
- Depends on: None
- Acceptance criteria:
- Table
user_preferencescreated with columns:user_id(UUID NOT NULL),key(VARCHAR(64) NOT NULL),value(TEXT NOT NULL),created_at(TIMESTAMPTZ DEFAULT NOW()),updated_at(TIMESTAMPTZ DEFAULT NOW()) - Primary key is
(user_id, key) - Index
idx_user_preferences_user_idcreated onuser_id - Migration is idempotent-safe (CREATE TABLE, not CREATE TABLE IF NOT EXISTS — rely on migration runner)
- Table
T5: PostgreSQL adapter - implement PreferenceRepository with sqlx
- Scope: Implement
PostgresPreferenceRepositorythat satisfies thePreferenceRepositoryinterface using sqlx queries.GetByUserIDselects all rows for a user.UpsertusesINSERT ... ON CONFLICT DO UPDATE. - Files:
- Create
services/preferences-api/internal/adapter/postgres/preference.go - Delete
services/preferences-api/internal/adapter/memory/example.go
- Create
- Depends on: T2, T4
- Acceptance criteria:
PostgresPreferenceRepositorystruct holds a*sqlx.DB(or pool frompkg/database)GetByUserIDexecutesSELECT user_id, key, value, created_at, updated_at FROM user_preferences WHERE user_id = $1UpsertexecutesINSERT INTO user_preferences ... ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()- Constructor
NewPreferenceRepository(db)returns a new instance - Implements
port.PreferenceRepositoryinterface (compile-time check)
T6: Handler layer - GET and PUT preference handlers with error mapping and handler tests
- Scope: Implement
PreferenceHandlerwithGetPreferencesandUpdatePreferencesHTTP handlers. GET extractsuser_idfrom URL, calls service, maps errors. PUT also binds request body viaapp.Bind. Write handler tests using chi test router with a mock service or mock repository. - Files:
- Create
services/preferences-api/internal/api/handlers/preference.go - Create
services/preferences-api/internal/api/handlers/preference_test.go - Delete
services/preferences-api/internal/api/handlers/example.go - Delete
services/preferences-api/internal/api/handlers/example_test.go
- Create
- Depends on: T3
- Acceptance criteria:
- GET handler extracts
user_idviachi.URLParam(r, "user_id") - GET handler returns 200 with
{data, meta}envelope containinguser_idandpreferences - GET handler returns 400 for invalid
user_id - PUT handler binds request body via
app.Bind() - PUT handler returns 200 with full merged preferences after update
- PUT handler returns 400 for unknown key, invalid value, or invalid user_id
- Both handlers return
errorwrapped withapp.Wrap() - Handler tests cover: successful GET, GET with defaults, successful PUT, PUT unknown key, PUT invalid value, invalid user_id for both endpoints
- Response shape matches spec:
{"data": {"user_id": "...", "preferences": {...}}, "meta": {...}}
- GET handler extracts
T7: Routes, OpenAPI spec, and main.go wiring
- Scope: Update
routes.goto register new preference routes (replacing example routes). Updatespec.goto document the new endpoints with schemas. Updatemain.goto wire database connection, run migrations, create PostgreSQL adapter, and inject into the service. - 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: T5, T6
- Acceptance criteria:
- Routes:
GET /api/preferences-api/preferences/{user_id}registered - Routes:
PUT /api/preferences-api/preferences/{user_id}registered - Routes: Old
/examplesroutes removed - Routes: Preference routes in auth-protectable group (conditional on
AUTH_ENABLED) - Routes: Health endpoint unchanged
- OpenAPI spec documents both endpoints with request/response schemas
- OpenAPI spec includes
UserPreferencesandUpdatePreferencesRequestschemas main.goconnects to database viapkg/databasemain.goruns migrations on startupmain.gocreatesPostgresPreferenceRepositoryand injects into servicemain.goadds DB pool shutdown hook
- Routes:
T8: Cleanup - remove example scaffold files
- Scope: Delete all remaining example scaffold files that were not already replaced in previous tasks. Verify no references to
exampleorExampleremain in the codebase underservices/preferences-api/. Ensure the service compiles and all tests pass. - Files:
- Delete any remaining
example*.gofiles - Delete
services/preferences-api/internal/adapter/memory/directory (if not already removed)
- Delete any remaining
- Depends on: T7
- Acceptance criteria:
- No files named
example*.goexist underservices/preferences-api/ - No references to
Example,example, ormemory.Newinservices/preferences-api/ go build ./...succeeds fromservices/preferences-api/go test ./...passes fromservices/preferences-api/go vet ./...passes fromservices/preferences-api/
- No files named