sqlbfx

package module
v0.0.0-...-726c145 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 19 Imported by: 0

README

sqlbfx — sqlb under uber-go/fx

The opinionated assembly of sqlb inside an fx application, and the contracts that make modules pluggable (ADR-0044). A separate Go module, so the engine's dependency gate does not move: fx, chi, goose and huma live here and reach nothing that does not import this package.

fx.New(
    fx.Supply(
        sqlbfx.DBConfig{DSN: os.Getenv("DATABASE_URL")},
        sqlbfx.HTTPConfig{Title: "Notes", Version: "1.0.0"},
    ),
    sqlbfx.Module(),

    store.Module,   // contributes migrations + generated operations
    auth.Module,    // contributes middleware, stores the principal
    notes.Module,   // contributes hooks that read the principal
).Run()

A feature module contributes to four value groups and provides nothing anybody imports:

Contribution Helper What it is
HookSet sqlbfx.ProvideHooks the module's query/mutation rules
MigrationSet sqlbfx.ProvideMigrations its embedded migration history
MiddlewareSet sqlbfx.ProvideMiddleware request wrapping, explicitly ordered
OperationSet sqlbfx.ProvideOperations its endpoints — with an error path that reaches the boot

Two properties are the point. A refused mount is a boot failure: the error sqlb raises for a Scoped resource with no confining hook (ADR-0030) travels out through OperationSet.Register and stops the process, naming the module. Ordering is a dependency edge: Migrated means "every registered migration set has been applied", the handles take one, and nothing can query a table that does not exist yet — in any module-list order.

The kit is five composable options. Module() is the sum for a standalone application; a codebase whose platform layer already owns the pool, the migrations and the router takes Handles() alone over the platform's *pgxpool.Pool and asserts the platform's guarantee with fx.Supply(sqlbfx.Migrated{}):

Option Provides
Pool() *pgxpool.Pool from DBConfig, closed by fx
Migrations() goose over the migrations group → Migrated
Handles() the hook registry from the hooks group → scoped *sqlb.DB + Unscoped
HTTP() chi + Huma over the middleware and operations groups, server lifetime
Module() all of the above

The auth seam is WithPrincipal / PrincipalFrom[T]: middleware verifies the request and stores the principal; scoping hooks read it back by type. Neither end names the other, which is what makes an auth mechanism swappable without touching a hook.

example/fxapp is the worked application — a tenant-scoped notes API on this kit, with the boot-refusal claim asserted against a real Postgres by removing a module and requiring the server not to start.

Documentation

Overview

Package sqlbfx assembles sqlb inside a uber-go/fx application, and owns the contracts that make modules pluggable.

It is the promotion of example/fxapp's hand-written glue into a module with an owner (ADR-0044): the pool and its lifetime, the migration runner over a value group, the hook registry assembled from what feature modules contribute, the scoped and unscoped handles, and an HTTP surface — chi, a Huma API, a server whose lifetime fx manages.

The contract is four value-group element types. A feature module provides some subset of them and nothing else:

sqlbfx.ProvideHooks(func(dir *spaces.Directory) sqlbfx.HookSet { ... })
sqlbfx.ProvideMigrations(func() sqlbfx.MigrationSet { ... })
sqlbfx.ProvideMiddleware(func(cfg Config) sqlbfx.MiddlewareSet { ... })
sqlbfx.ProvideOperations(func(db *sqlb.DB) sqlbfx.OperationSet { ... })

Two properties the kit preserves are the reason it exists rather than being copied per application. A refused mount is a boot failure: OperationSet's Register returns an error, and the error sqlb raises for a Scoped resource with no confining hook (ADR-0030) reaches fx and stops the process, naming the module. And ordering is a dependency edge: the Migrated value means "every registered migration set has been applied", the handles take one, so nothing can query a table that does not exist yet — in any module-list order.

The kit is opinionated where the engine is not: chi for the router, humachi for the API, goose for the migration runner, log/slog for the log. An application that holds different opinions writes its own kit; this package's source is small and states what such a kit must preserve.

Configuration is the application's business. DBConfig and HTTPConfig are plain structs the application provides (fx.Supply, or a constructor that reads whatever the application reads); the kit reads no environment variable. The logger is optional: a *slog.Logger in the graph is used, otherwise slog.Default() — the kit never provides one.

Index

Constants

View Source
const (
	GroupHooks      = "sqlbfx.hooks"
	GroupMigrations = "sqlbfx.migrations"
	GroupMiddleware = "sqlbfx.middleware"
	GroupOperations = "sqlbfx.operations"
)

The value-group names, namespaced so a library cannot collide with an application's own groups. Modules normally never spell these: the Provide* helpers below carry them.

Variables

This section is empty.

Functions

func DB

func DB() fx.Option

DB is the database half of the kit: the pool with its lifetime, the migration runner over the migrations group, and the two handles. Take it alone for a process with no HTTP surface — a worker, a migration job.

The application must provide a DBConfig (fx.Supply, or a constructor that reads whatever the application reads).

func HTTP

func HTTP() fx.Option

HTTP is the HTTP half: chi with the middleware group installed, a Huma API with the operations group registered on it, and a server whose lifetime fx manages.

The application must provide an HTTPConfig.

func Handles

func Handles() fx.Option

Handles is the sqlb layer alone: the hook registry assembled from the hooks group, the scoped handle, and the Unscoped one.

It consumes a *pgxpool.Pool and a Migrated wherever they come from. In a standalone application that is Pool() and Migrations(); in an application whose platform layer already owns the pool and applies migrations its own way — the studio-apps/core shape, where dbbase is a module of the platform's — the platform provides the pool and the application states the fact the platform established:

platform.DBModule,                  // provides *pgxpool.Pool, runs its own migrations
fx.Supply(sqlbfx.Migrated{}),       // "and they have run before anything queries"
sqlbfx.Handles(),

Supplying Migrated is an assertion, and it is the application's to make: the kit cannot know what the platform's runner guarantees.

func Migrations

func Migrations() fx.Option

Migrations is the runner over the migrations group, producing Migrated.

func Module

func Module() fx.Option

Module is the whole kit: DB and HTTP together. The application provides DBConfig and HTTPConfig, contributes to the four groups, and lists this beside its own modules.

func Pool

func Pool() fx.Option

Pool is the pgx pool alone: opened from the application's DBConfig, closed by fx. Take it separately when the platform owns everything else.

func PrincipalFrom

func PrincipalFrom[T any](ctx context.Context) (T, bool)

PrincipalFrom returns the principal as a T, and whether one of that type was stored.

The two failure modes are deliberately one answer: no principal stored, and a principal of a different type, both report false. A hook that needs the distinction is coupling itself to which middleware ran — the thing this seam exists to prevent.

func ProvideHooks

func ProvideHooks(ctor any) fx.Option

ProvideHooks provides a constructor whose result joins the hooks group. The constructor may take any dependencies; its result must be HookSet.

func ProvideMiddleware

func ProvideMiddleware(ctor any) fx.Option

ProvideMiddleware provides a constructor whose result joins the middleware group. The constructor may take any dependencies; its result must be MiddlewareSet.

func ProvideMigrations

func ProvideMigrations(ctor any) fx.Option

ProvideMigrations provides a constructor whose result joins the migrations group. The constructor may take any dependencies; its result must be MigrationSet.

func ProvideOperations

func ProvideOperations(ctor any) fx.Option

ProvideOperations provides a constructor whose result joins the operations group. The constructor may take any dependencies; its result must be OperationSet.

func WithPrincipal

func WithPrincipal(ctx context.Context, p any) context.Context

WithPrincipal returns a context carrying p as the request's principal.

Middleware calls this once, after verifying whatever the request presented. Storing an unverified value here defeats every hook that trusts it, which is the same property the example's access module states about a plain X-Tenant header: a boundary a caller can name is a convention, not a boundary.

Types

type DBConfig

type DBConfig struct {
	// DSN is the Postgres connection string. Required.
	DSN string

	// MaxConns and MinIdleConns size the pool; zero keeps pgxpool's default.
	// Behind PgBouncer in transaction pooling mode these numbers mean
	// something different again — see ADR-0019.
	MaxConns     int32
	MinIdleConns int32

	// ConnMaxLifetime bounds a connection's age; zero keeps pgxpool's
	// default.
	ConnMaxLifetime time.Duration

	// ConnectTimeout bounds the ping before migrations run. A server that
	// cannot reach its database should fail while somebody is watching the
	// deploy, not hang. Zero means 10 seconds.
	ConnectTimeout time.Duration
}

DBConfig is what the pool needs. The application provides it — from env, from flags, from wherever; the kit reads no environment variable, because how the pool is sized and where its DSN comes from is the application's business (ADR-0040).

type HTTPConfig

type HTTPConfig struct {
	// Addr is the listen address, ":8080" when empty. ":0" is valid and is
	// what a test asks for; the bound address is on the *http.Server's Addr
	// after start.
	Addr string

	// Title and Version go into the OpenAPI document.
	Title   string
	Version string

	// ShutdownTimeout bounds the graceful shutdown on stop. Zero means 15
	// seconds.
	ShutdownTimeout time.Duration

	// Huma, when set, edits the OpenAPI config before the API is built —
	// the document description, security schemes, whatever the application
	// owns. The kit's only opinions in the document are Title and Version.
	Huma func(*huma.Config)
}

HTTPConfig is what the HTTP surface needs. The application provides it, like DBConfig.

type HookSet

type HookSet struct {
	// Module names the contributor. It appears in the boot log and in the
	// error when Register fails, which is the difference between "a hook
	// failed" and a file to open.
	Module string

	// Register adds this module's rules to the registry.
	Register func(*sqlb.Registry) error
}

HookSet is the value-group element a module contributes to register its query and mutation rules.

Register may fail: a rule that cannot be expressed is better reported at boot than skipped, and the group's whole purpose is that nobody downstream gets a handle until every contributor has had its say.

type MiddlewareSet

type MiddlewareSet struct {
	// Module names the contributor, for the boot log.
	Module string

	// Order decides where in the chain this sits: lower runs first, and ties
	// break on Module so the chain is the same on every boot.
	//
	// An explicit number rather than the order the group happened to arrive
	// in, because value-group order is not something fx promises — and
	// because middleware order is a correctness question. Authentication that
	// ran after the handler would be decoration.
	Order int

	// Wrap is the middleware.
	Wrap func(http.Handler) http.Handler
}

MiddlewareSet is the value-group element a module contributes to wrap every request.

type Migrated

type Migrated struct{}

Migrated is the fact that every registered migration set has been applied.

It is a value rather than an fx.Invoke because ordering in a container is a dependency edge, not a position in a list: anything that reads or writes a table takes a Migrated, and is then constructed after this ran by construction rather than by everyone remembering to list the kit first. The handles take one, so every query through them is downstream of it.

Applying migrations at startup suits a demo and a single-instance service. It does not suit a rolling deploy, where several new instances race to apply the same migration and the old code briefly runs against the new schema. There, migrations are a deployment step that finishes before any new instance starts — replace the kit's runner (fx.Decorate on Migrated is not enough; provide DB() without Module and produce Migrated from a version assertion instead).

type MigrationSet

type MigrationSet struct {
	// Module names the set. It is the prefix of the tracking table
	// (<Module>_schema_migrations), so two modules migrate independently and
	// neither can renumber the other's history.
	Module string

	// FS is the embedded filesystem holding the .sql files.
	FS fs.FS

	// Dir is the path within FS the files live at, "." when they are at the
	// root.
	Dir string
}

MigrationSet is the value-group element a module contributes to register its migration history.

type OperationSet

type OperationSet struct {
	// Module names the contributor, for the boot log and the error.
	Module string

	// Register is called once, with the API every module shares.
	Register func(huma.API) error
}

OperationSet is the value-group element a module contributes to put endpoints on the shared API.

Register returning an error is what carries a refused mount out to the boot: the generated Register reports a resource whose declared scope has no hook behind it (ADR-0030), and this group is how that reaches fx instead of being logged and stepped over.

type Unscoped

type Unscoped struct {
	*sqlb.DB
}

Unscoped is the handle with no hooks on it.

It exists for the jobs that cannot be scoped because they run before there is anything to scope by: provisioning tenants at boot, and resolving a credential to the id the hooks then filter on.

It is a distinct type rather than a flag on the scoped handle, and that is the point: a flag is something a caller passes, and the set of callers allowed to pass it is exactly the thing being controlled. A type is also grep-able the same way the example's `name:"unscoped"` tag was, and cannot be misspelled in a string only fx.ValidateApp would catch. Grep for sqlbfx.Unscoped to see every consumer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL