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/
This commit is contained in:
117
internal/application/launcher.go
Normal file
117
internal/application/launcher.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user