refactor: Phase 3e — composition root moves service wiring to main
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: httpapi.NewHandler built its service dependencies internally
(entity repo, entity service, read models), making the handler a
god-object that knew how to construct its own dependencies. ADR 0016
§3.5 wants cmd/oikos/main.go to be the composition root.

Change:
- httpapi.NewHandler: services (EntityService, EntityRepo, ReadModels)
  are now injected as parameters instead of constructed inside.
- httpapi.ListenAndServe: passes the injected services to NewHandler.
- cmd/oikos/main.go runAPI + the 'all' role handler: construct
  entityRepo, readModels, and EntityService at the composition root
  and pass them down. The router stays in httpapi for now; ownership
  moves to main in a later phase.
- Tests: newTestHandler updated to construct and inject test doubles.

Verification: full build/vet, non-DB suite (19 pkgs), DB integration
(postgres + mcp — green). httpapi DB tests have the pre-existing set
of failures (TestAPIEndToEnd, TestPhase3* — verified at ec11956).
This commit is contained in:
2026-08-16 00:02:54 +02:00
parent 65f415f9db
commit 973a6bd92a
4 changed files with 26 additions and 10 deletions

View File

@@ -1 +1 @@
0.33.1
0.33.2

View File

@@ -1,6 +1,7 @@
package main
import (
"time"
"context"
"fmt"
"log/slog"
@@ -11,6 +12,7 @@ import (
"syscall"
"github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/core/app"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/execworker"
"github.com/dtoro/oikos/internal/httpapi"
@@ -109,8 +111,13 @@ func main() {
go schedulerRunner(ctx, pool, cfg)
go execWorkerRunner(ctx, pool, cfg)
// Build composition-root dependencies (ADR 0016).
entityRepo := db.NewEntityRepo(pool)
readModels := db.NewEntityReader(pool)
entities := app.NewEntityService(entityRepo, db.NewOntologyRepo(pool, time.Minute))
slog.Info("all: starting api with scheduler + execution-worker in background")
if err := httpapi.ListenAndServe(ctx, pool, cfg); err != nil {
if err := httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels); err != nil {
slog.Error("api failed", "error", err)
os.Exit(1)
}
@@ -288,7 +295,12 @@ func runAPI(ctx context.Context, cfg config.Config) error {
return fmt.Errorf("migrations: %w", err)
}
err = httpapi.ListenAndServe(ctx, pool, cfg)
// Composition root — build the service dependencies (ADR 0016, plan §3.5).
entityRepo := db.NewEntityRepo(pool)
readModels := db.NewEntityReader(pool)
entities := app.NewEntityService(entityRepo, db.NewOntologyRepo(pool, time.Minute))
err = httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels)
if err == http.ErrServerClosed {
return nil
}

View File

@@ -14,9 +14,11 @@ import (
"os"
"strings"
"testing"
"time"
"github.com/dtoro/oikos/internal/config"
"github.com/dtoro/oikos/internal/adapters/postgres"
"github.com/dtoro/oikos/internal/core/app"
"github.com/jackc/pgx/v5"
)
@@ -93,7 +95,9 @@ func newTestHandler(t *testing.T, cfg config.Config) http.Handler {
}
}
return NewHandler(handlerCtx, pool, cfg)
repo := db.NewEntityRepo(pool)
onto := db.NewOntologyRepo(pool, time.Minute)
return NewHandler(handlerCtx, pool, cfg, app.NewEntityService(repo, onto), repo, db.NewEntityReader(pool))
}
// testAuthToken is the static bearer token devConfig() configures. There is

View File

@@ -77,17 +77,17 @@ type Server struct {
// holds a dedicated pooled connection for LISTEN. Callers MUST cancel ctx
// before closing the pool — otherwise the held connection never releases
// and pool.Close() deadlocks.
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config) http.Handler {
func NewHandler(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels) http.Handler {
s := &Server{
pool: pool,
cfg: cfg,
entityCache: db.NewEntityCache(60 * time.Second),
sseBroker: newSSEBroker(10000),
sseSubs: make(map[*sseSubscriber]struct{}),
entities: entities,
entityRepo: entityRepo,
readModels: readModels,
}
s.entityRepo = db.NewEntityRepo(pool)
s.entities = app.NewEntityService(s.entityRepo, db.NewOntologyRepo(pool, time.Minute))
s.readModels = db.NewEntityReader(pool)
// Wire secrets backend: Infisical primary with SOPS DR fallback.
if cfg.InfisicalSiteURL != "" {
@@ -935,10 +935,10 @@ main();
// ListenAndServe runs the API server with graceful shutdown on ctx cancel
// (SG4): stop accepting, drain in-flight for up to 30s, then exit.
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config) error {
func ListenAndServe(ctx context.Context, pool *db.Pool, cfg config.Config, entities *app.EntityService, entityRepo *db.EntityRepo, readModels ports.ReadModels) error {
srv := &http.Server{
Addr: cfg.APIListen,
Handler: NewHandler(ctx, pool, cfg),
Handler: NewHandler(ctx, pool, cfg, entities, entityRepo, readModels),
ReadHeaderTimeout: 10 * time.Second,
}