Files
todo-api/internal/application/launcher.go
Rene Nochebuena 3fcba82448 feat(todo-api): add full-stack POC demonstrating micro-lib v0.9.0
Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement.

What's included:
- cmd/todo-api: minimal main delegating to application.Run
- internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart
- internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions
- internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc)
- internal/service: TodoService, UserService with interface-based dependencies
- internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation
- internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate)
- logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface
- SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart
- Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC)

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-19 13:55:08 +00:00

118 lines
3.7 KiB
Go

package application
import (
"context"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"code.nochebuena.dev/go/httpserver"
"code.nochebuena.dev/go/httpmw"
"code.nochebuena.dev/go/launcher"
"code.nochebuena.dev/go/logz"
"code.nochebuena.dev/go/sqlite"
"code.nochebuena.dev/go/valid"
"code.nochebuena.dev/go/todo-api/internal/domain"
"code.nochebuena.dev/go/todo-api/internal/handler"
appMW "code.nochebuena.dev/go/todo-api/internal/middleware"
"code.nochebuena.dev/go/todo-api/internal/repository"
"code.nochebuena.dev/go/todo-api/internal/service"
)
const migrate = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_role (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
resource TEXT NOT NULL,
permissions INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, resource)
);
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
// logAdapter bridges logz.Logger (With returns logz.Logger) to httpmw.Logger
// (With must return httpmw.Logger). Defined locally — no extra package needed.
type logAdapter struct{ l logz.Logger }
func (a *logAdapter) Info(msg string, args ...any) { a.l.Info(msg, args...) }
func (a *logAdapter) Error(msg string, err error, args ...any) { a.l.Error(msg, err, args...) }
func (a *logAdapter) With(args ...any) httpmw.Logger { return &logAdapter{a.l.With(args...)} }
// Run wires all dependencies, starts the launcher, and blocks until shutdown.
func Run() error {
logger := logz.New(logz.Options{JSON: false})
// --- Infrastructure ---
db := sqlite.New(logger, sqlite.Config{Path: "todos.db"})
// --- Repositories ---
todoRepo := repository.NewTodoRepository(db)
userRepo := repository.NewUserRepository(db)
permProvider := repository.NewPermissionProvider(db)
// --- Services ---
v := valid.New()
todoSvc := service.NewTodoService(todoRepo)
userSvc := service.NewUserService(userRepo, uuid.NewString)
// --- Handlers ---
todoH := handler.NewTodoHandler(todoSvc, v)
userH := handler.NewUserHandler(userSvc, v)
// --- HTTP server ---
srv := httpserver.New(logger, httpserver.Config{Port: 3000},
httpserver.WithMiddleware(
httpmw.RequestID(uuid.NewString),
httpmw.Recover(),
httpmw.RequestLogger(&logAdapter{logger}),
),
)
// --- Launcher ---
lc := launcher.New(logger)
lc.Append(db, srv)
lc.BeforeStart(func() error {
// Migrations run after db.OnInit, before srv.OnStart.
ctx := context.Background()
if _, err := db.GetExecutor(ctx).ExecContext(ctx, migrate); err != nil {
return err
}
// POST /users — open; no auth required (bootstrapping)
srv.Post("/users", userH.Create)
// All other routes require a valid X-User-ID (Auth middleware populates rbac.Identity).
srv.Group(func(r chi.Router) {
r.Use(appMW.Auth(userRepo))
// Todos — permission-gated
r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)).
Get("/todos", todoH.FindAll)
r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermWriteTodo)).
Post("/todos", todoH.Create)
// Users — read-only list, also requires read permission on todos
// (keeps the surface small; demonstrates RBAC on a second endpoint)
r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)).
Get("/users", userH.FindAll)
})
return nil
})
return lc.Run()
}