8.5 KiB
8.5 KiB
Tasks: User Preferences API
Task Order (dependency sequence)
T1: Domain layer - UserPreferences model and domain errors
- Scope: Create the
UserPreferencesdomain model and preference-specific domain errors. The model usesmap[string]anyfor flexible key-value storage. AddErrInvalidUserIDandErrInvalidPreferenceValuesentinel errors. - Files:
- Create
services/preferences-api/internal/domain/preference.go - Modify
services/preferences-api/internal/domain/errors.go(replace example errors with preference errors)
- Create
- Depends on: None
- Acceptance criteria:
UserPreferencesstruct exists with fields:UserID string,Preferences map[string]any,CreatedAt time.Time,UpdatedAt time.TimeErrInvalidUserIDsentinel error definedErrInvalidPreferenceValuesentinel error defined- Example domain errors removed
- Package compiles without errors
T2: Port layer - PreferenceRepository interface
- Scope: Define the
PreferenceRepositoryinterface withGetandUpsertmethods.Getreturnsnil, nilfor non-existent users (not an error).Upserthandles both create and update atomically. - Files:
- Create
services/preferences-api/internal/port/preference.go
- Create
- Depends on: T1
- Acceptance criteria:
PreferenceRepositoryinterface defined withGet(ctx context.Context, userID string) (*domain.UserPreferences, error)methodPreferenceRepositoryinterface hasUpsert(ctx context.Context, prefs *domain.UserPreferences) errormethod- Method signatures document expected nil behavior for
Get(nil, nil when no row exists) - Package compiles without errors
T3: Service layer - PreferenceService with validation logic and tests
- Scope: Implement
PreferenceServicewithGetandUpsertmethods. TheUpsertmethod validates known preference keys (theme,language,notifications_enabled), merges incoming preferences with existing ones, and delegates to the repository. Includes a customValidationErrortype that wrapsdomain.ErrInvalidPreferenceValueand carries per-field error details. 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
- Create
- Depends on: T1, T2
- Acceptance criteria:
PreferenceServicestruct with constructorNewPreferenceService(repo, logger)Getmethod delegates to repository and returns resultUpsertmethod validatesthemeagainst["light", "dark", "system"]Upsertmethod validateslanguageas a valid BCP-47 tagUpsertmethod validatesnotifications_enabledas boolean- Unknown preference keys accepted without validation
- Multiple validation errors collected and returned together in
ValidationError ValidationErrortype implementserrorandUnwrap()returningdomain.ErrInvalidPreferenceValueValidationError.Detailsismap[string]stringwith per-field messages- Merge strategy: incoming keys overwrite existing, unmentioned keys preserved
- Tests cover: valid preferences, invalid theme, invalid language, invalid notifications_enabled, multiple errors, unknown keys accepted, merge behavior
- All tests pass
T4: Database migration and PostgreSQL adapter
- Scope: Create the SQL migration for the
preferencestable and implement the PostgreSQL adapter forPreferenceRepository. Uses sqlx with parameterized queries.Getreturnsnil, nilonsql.ErrNoRows.UpsertusesON CONFLICTfor atomic create-or-update. - Files:
- Create
services/preferences-api/migrations/001_create_preferences.sql - Create
services/preferences-api/internal/adapter/postgres/preference.go
- Create
- Depends on: T1, T2
- Acceptance criteria:
- Migration creates
preferencestable with columns:user_id UUID PRIMARY KEY,preferences JSONB NOT NULL DEFAULT '{}',created_at TIMESTAMPTZ,updated_at TIMESTAMPTZ - Migration creates index
idx_preferences_updated_atonupdated_at - Migration uses
CREATE TABLE IF NOT EXISTSfor idempotency - PostgreSQL adapter implements
PreferenceRepositoryinterface (compile-time check) Getuses parameterized query and returnsnil, nilforsql.ErrNoRowsUpsertusesINSERT ... ON CONFLICT ... DO UPDATEwith parameterized values- JSONB marshaling/unmarshaling handled correctly for
map[string]any - Package compiles without errors
- Migration creates
T5: HTTP handlers - GET and PUT preference endpoints with tests
- Scope: Implement preference handlers for GET and PUT. GET extracts
user_idfrom URL, validates UUID format, calls service, and returns response in{data, meta}envelope. PUT additionally binds the request body and maps validation errors to structured HTTP error responses. Write handler tests using httptest with a mock service/repository. - Files:
- Create
services/preferences-api/internal/api/handlers/preference.go - Create
services/preferences-api/internal/api/handlers/preference_test.go
- Create
- Depends on: T1, T2, T3
- Acceptance criteria:
Preferencehandler struct with constructorNewPreference(svc, logger)Gethandler extractsuser_idviachi.URLParam, validates UUID, calls service, returnshttpresponse.OKGethandler returns 200 with empty preferences{}when service returns nilGethandler returns 400 for invalid UUID formatUpserthandler binds request body withapp.Bind(), validates preferences field is not nilUpserthandler calls service and returnshttpresponse.OKwith updated preferencesUpserthandler mapsValidationErrortohttperrorwith detailsPreferenceResponsestruct hasjsontags foruser_id,preferences,updated_atmapDomainErrorfunction handlesErrInvalidPreferenceValuewith details- Tests cover: GET success, GET empty preferences, GET invalid UUID, PUT success, PUT validation error, PUT missing body
- All tests pass
T6: Routes, OpenAPI spec, and main.go wiring
- Scope: Update routes to register preference endpoints (replacing example routes). Update OpenAPI spec to document preference schemas and endpoints (removing example schemas). Update main.go to wire PostgreSQL connection, run migrations, create postgres adapter, and inject into the service. Add
golang.org/x/text/languagedependency for BCP-47 validation. - 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
services/preferences-api/go.mod(addgolang.org/x/textdependency)
- Modify
- Depends on: T3, T4, T5
- Acceptance criteria:
- Routes register
GET /preferences/{user_id}andPUT /preferences/{user_id}under/api/preferences-api - Example routes removed
- Both routes wrapped with
app.Wrap() - OpenAPI spec defines
UserPreferencesschema andUpdatePreferencesRequestschema - OpenAPI spec documents GET and PUT endpoints with parameters, request bodies, and response schemas
- Example schemas removed from spec
main.goconnects to PostgreSQL viadatabase.MustConnectmain.goruns migrations viadatabase.MustRunMigrationsmain.gocreatespostgres.NewPreferenceRepositoryandservice.NewPreferenceServicemain.godeferspool.Close()golang.org/x/text/languagedependency added- Service compiles and starts successfully
- Routes register
T7: Cleanup - Remove example scaffolding files
- Scope: Delete all example-related files that have been replaced by preference implementations. This is done last to ensure nothing breaks.
- Files:
- Delete
services/preferences-api/internal/domain/example.go - Delete
services/preferences-api/internal/service/example.go - Delete
services/preferences-api/internal/service/example_test.go - Delete
services/preferences-api/internal/port/example.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: T6
- Acceptance criteria:
- All example files deleted
- No imports reference example types
go test ./...passes with no compilation errors- Service compiles cleanly