build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
66fcd0cab1
commit
208033482e
@ -22,7 +22,7 @@ artifacts:
|
||||
status: pending
|
||||
path: review.md
|
||||
spec:
|
||||
status: pending
|
||||
status: draft
|
||||
path: spec.md
|
||||
tasks:
|
||||
status: pending
|
||||
|
||||
150
.sdlc/features/user-preferences/spec.md
Normal file
150
.sdlc/features/user-preferences/spec.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Feature: User Preferences API
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Users of the platform need a way to persist and retrieve their personal preferences (theme, language, notification settings) across sessions and devices. Currently the preferences-api service exists as a scaffold with only example CRUD endpoints and an in-memory store. There is no real preferences domain, no database persistence, and no API for managing user preferences.
|
||||
|
||||
Application frontends need a reliable backend API to read and write per-user preference key-value pairs so that UI settings survive page refreshes, device switches, and service restarts.
|
||||
|
||||
## User Stories
|
||||
|
||||
- As a **frontend application**, I want to GET a user's preferences so that I can render the UI with their chosen theme, language, and notification settings.
|
||||
- As a **frontend application**, I want to PUT (upsert) a user's preferences so that changes to settings are persisted immediately.
|
||||
- As a **platform operator**, I want preferences stored in PostgreSQL so that they survive service restarts and are backed up with the rest of the data.
|
||||
- As a **developer**, I want default preference values defined server-side so that new users get sensible defaults without client-side logic.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for the given user as a JSON object
|
||||
- [ ] `GET` for a user with no stored preferences returns server-defined defaults (not 404)
|
||||
- [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences for the given user (full replace of provided keys)
|
||||
- [ ] `PUT` is idempotent -- calling it twice with the same body produces the same result
|
||||
- [ ] Preferences are stored as key-value pairs in PostgreSQL
|
||||
- [ ] The following preference keys are supported with validation:
|
||||
- `theme` -- string, one of: `light`, `dark`, `system` (default: `system`)
|
||||
- `language` -- string, BCP 47 language tag, validated format (default: `en`)
|
||||
- `notifications_enabled` -- boolean (default: `true`)
|
||||
- [ ] Unknown preference keys in a PUT request are rejected with 400 Bad Request
|
||||
- [ ] `user_id` path parameter is validated as a non-empty string (UUID format)
|
||||
- [ ] All responses use the standard `{data, meta}` envelope
|
||||
- [ ] OpenAPI spec is updated with the new endpoints and schemas
|
||||
- [ ] Database migration creates the `user_preferences` table
|
||||
- [ ] Existing example CRUD endpoints and domain are removed (replaced by preferences)
|
||||
- [ ] Handler, service, domain, port, and adapter layers follow hexagonal architecture
|
||||
- [ ] Unit tests cover service logic (defaults, validation, upsert behavior)
|
||||
- [ ] Handler tests cover HTTP layer (request binding, error responses, status codes)
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
- **Database**: PostgreSQL via `pkg/database` (sqlx). Migration files in `services/preferences-api/migrations/`.
|
||||
- **Port**: Service runs on port 8001 (already configured).
|
||||
- **URL routing**: Must use brace syntax `{user_id}` (chi router). Never colon syntax.
|
||||
- **Error handling**: Domain errors mapped to HTTP errors via `httperror.*`. Handlers return `error`, wrapped with `app.Wrap()`.
|
||||
- **Request binding**: Use `app.BindAndValidate()` for PUT request body.
|
||||
- **Response format**: Use `httpresponse.OK()` / `httpresponse.NoContent()` for responses.
|
||||
- **Auth**: Auth middleware is opt-in via config. Endpoints should be in an auth-protectable route group, but auth enforcement is not required for this feature (configurable via `AUTH_ENABLED`).
|
||||
- **Preference storage model**: Each preference is a row in the `user_preferences` table with columns: `user_id`, `key`, `value`, `created_at`, `updated_at`. This allows adding new preference keys without schema changes.
|
||||
- **Defaults**: When a stored preference is missing, the API merges server-defined defaults so the response always contains all known keys.
|
||||
|
||||
## API Design
|
||||
|
||||
### GET /api/preferences-api/preferences/{user_id}
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"notifications_enabled": true
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"request_id": "...",
|
||||
"timestamp": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PUT /api/preferences-api/preferences/{user_id}
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "fr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only provided keys are updated. Omitted keys retain their current value (or default if never set).
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"language": "fr",
|
||||
"notifications_enabled": true
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"request_id": "...",
|
||||
"timestamp": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error 400 (unknown key):**
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "unknown preference key: font_size"
|
||||
},
|
||||
"meta": { "..." }
|
||||
}
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_preferences (
|
||||
user_id UUID NOT NULL,
|
||||
key VARCHAR(64) NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (user_id, key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_preferences_user_id ON user_preferences (user_id);
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- PostgreSQL database accessible via `DATABASE_URL` environment variable
|
||||
- `pkg/database` for connection pooling and migrations
|
||||
- `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` for HTTP layer
|
||||
- `pkg/auth` for optional authentication middleware
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Per-preference-key access control (all preferences for a user are readable/writable as a unit)
|
||||
- DELETE endpoint for individual preferences (not in requirements)
|
||||
- Preference history / audit log
|
||||
- Bulk operations across multiple users
|
||||
- WebSocket push for real-time preference sync
|
||||
- Admin endpoints for managing preference definitions
|
||||
- Frontend integration (separate feature)
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Authorization model**: Should the API enforce that `user_id` in the path matches the authenticated user's ID? Or is cross-user preference access allowed (e.g., for admin tools)? *Current assumption: no enforcement, auth is opt-in via config.*
|
||||
2. **Preference value types**: Should values be typed (string/bool/number) at the API level, or stored/returned as strings with client-side parsing? *Current assumption: typed in API response (theme as string, notifications_enabled as boolean), stored as TEXT in DB with serialization.*
|
||||
3. **Partial vs full update semantics**: PUT currently does partial update (merge). Should it be full replace (all keys must be provided)? *Current assumption: partial merge -- only provided keys are updated, missing keys retain current values.*
|
||||
Loading…
Reference in New Issue
Block a user