Files
todo-api/CHANGELOG.md

77 lines
6.4 KiB
Markdown
Raw Normal View History

# Changelog
All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.9.0-poc] - 2026-03-18
### Added
This entry describes what the POC demonstrates, not a versioned library API surface.
**Application structure**
- N-layer architecture: `cmd/todo-api``internal/application` (wiring only) → `internal/handler``internal/service``internal/repository``internal/domain`; each layer depends on its inner neighbours via interfaces, with `application` as the only package that owns the full object graph
- `application.Run()` — top-level entry point; wires all dependencies, registers routes inside `lc.BeforeStart`, and calls `lc.Run()` to start the launcher lifecycle
- Local `logAdapter` struct in `application` — bridges `logz.Logger` (whose `With` returns `logz.Logger`) to `httpmw.Logger` (whose `With` must return `httpmw.Logger`), demonstrating the pattern for resolving return-type mismatches between micro-lib interfaces without modifying either library
**Domain layer** (`internal/domain`)
- `Todo` struct — `ID int64`, `Title string`, `Done bool`, `CreatedAt time.Time`, JSON-tagged
- `User` struct — `ID string`, `Name string`, `Email string`, `CreatedAt time.Time`, JSON-tagged
- `ResourceTodos = "todos"` constant — the resource key used in the `user_role` table
- `PermReadTodo rbac.Permission = 0` and `PermWriteTodo rbac.Permission = 1` — bit positions for the todos resource permission mask
**Repository layer** (`internal/repository`)
- `TodoRepository` interface — `FindAll(ctx) ([]domain.Todo, error)`, `Create(ctx, domain.Todo) (domain.Todo, error)`
- `UserRepository` interface — `FindAll`, `FindByID`, `Create`, `SetPermissions(ctx, userID, resource string, mask rbac.PermissionMask) error`
- `NewTodoRepository(db sqlite.Client) TodoRepository` — SQLite-backed implementation; `FindAll` returns an empty slice (never nil) when no rows exist; `Create` uses `RETURNING` to retrieve the generated ID and timestamp
- `NewUserRepository(db sqlite.Client) UserRepository` — SQLite-backed implementation; `FindByID` returns a `xerrors.ErrNotFound`-wrapped error on `sql.ErrNoRows`; `SetPermissions` uses `INSERT ... ON CONFLICT DO UPDATE` for upsert
- `DBPermissionProvider` struct implementing `rbac.PermissionProvider` — resolves the integer bitmask from `user_role` for a given `(uid, resource)` pair; returns mask `0` (no permissions) for a missing row rather than an error
- `NewPermissionProvider(db sqlite.Client) *DBPermissionProvider`
**Service layer** (`internal/service`)
- `CreateTodoRequest` struct — `Title string` with `validate:"required,min=1,max=255"` tag
- `TodoService` interface — `FindAll(ctx) ([]domain.Todo, error)`, `Create(ctx, CreateTodoRequest) (domain.Todo, error)`
- `NewTodoService(repo repository.TodoRepository) TodoService`
- `CreateUserRequest` struct — `Name string`, `Email string`, `CanRead bool`, `CanWrite bool`, with validation tags; `CanRead`/`CanWrite` seed the permission bitmask for `ResourceTodos` at creation time
- `UserService` interface — `FindAll(ctx) ([]domain.User, error)`, `Create(ctx, CreateUserRequest) (domain.User, error)`
- `NewUserService(repo repository.UserRepository, idGen func() string) UserService``idGen` is injected (e.g. `uuid.NewString`) to mint user IDs
**Handler layer** (`internal/handler`)
- `TodoHandler``NewTodoHandler(svc service.TodoService, v valid.Validator) *TodoHandler`; `FindAll` delegates to `httputil.HandleNoBody[[]domain.Todo]`; `Create` delegates to `httputil.Handle[CreateTodoRequest, domain.Todo]`
- `UserHandler``NewUserHandler(svc service.UserService, v valid.Validator) *UserHandler`; `FindAll` and `Create` follow the same `httputil` adapter pattern
**Middleware layer** (`internal/middleware`)
- `Auth(userRepo repository.UserRepository) func(http.Handler) http.Handler` — reads `X-User-ID` header, looks up the user, and stores an `rbac.Identity` in context via `rbac.SetInContext`; returns 401 JSON on missing header or unknown user ID; the output contract is identical to what `httpauth-firebase.EnrichmentMiddleware` produces, so swapping in real Firebase auth requires no changes to handlers or services
- `Require(provider rbac.PermissionProvider, resource string, perms ...rbac.Permission) func(http.Handler) http.Handler` — reads `rbac.Identity` from context, resolves the permission mask, and enforces each required bit; returns 403 JSON on missing identity or insufficient permissions; returns 500 JSON if the provider errors
**Micro-lib stack composition demonstrated**
- `launcher` orchestrating `sqlite` and `httpserver` through `OnInit → OnStart → OnStop`
- `httpserver` with `RequestID → Recover → RequestLogger` middleware via `httpmw`
- `sqlite` (modernc pure-Go, no CGO) as the persistence layer with inline schema migrations run in `lc.BeforeStart`
- `rbac` identity and permission types as the shared contract across middleware, repository, and domain
- `valid` for request body validation in handlers
- `httputil` handler adapters (`Handle`, `HandleNoBody`) for all route handlers
- `xerrors` `ErrNotFound` for typed not-found errors in the repository layer
- Route registration in `lc.BeforeStart` so migrations complete before any route is reachable and the port is not bound until routes are registered
**Routes**
- `POST /users` — open; no auth required (user bootstrapping)
- `GET /todos` — requires `Auth` middleware and `PermReadTodo` permission
- `POST /todos` — requires `Auth` middleware and `PermWriteTodo` permission
- `GET /users` — requires `Auth` middleware and `PermReadTodo` permission on the todos resource
### Design Notes
- Header-based auth (`X-User-ID`) deliberately mirrors the `rbac.Identity` output contract of `httpauth-firebase`, proving that the RBAC contract is provider-agnostic; the POC can be upgraded to real Firebase auth by replacing the `Auth` middleware with no changes to any other layer
- SQLite with no Docker dependency keeps the POC self-contained: a single `go run ./cmd/todo-api` is sufficient to run the full stack, with the database created as `todos.db` in the working directory on first run
- Routes are registered in `lc.BeforeStart` rather than at construction time, making the lifecycle ordering explicit and auditable in a single closure: migrations run after `db.OnInit`, routes register after migrations, and `srv.OnStart` binds the port last