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
- func DB() fx.Option
- func HTTP() fx.Option
- func Handles() fx.Option
- func Migrations() fx.Option
- func Module() fx.Option
- func Pool() fx.Option
- func PrincipalFrom[T any](ctx context.Context) (T, bool)
- func ProvideHooks(ctor any) fx.Option
- func ProvideMiddleware(ctor any) fx.Option
- func ProvideMigrations(ctor any) fx.Option
- func ProvideOperations(ctor any) fx.Option
- func WithPrincipal(ctx context.Context, p any) context.Context
- type DBConfig
- type HTTPConfig
- type HookSet
- type MiddlewareSet
- type Migrated
- type MigrationSet
- type OperationSet
- type Unscoped
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
Migrations is the runner over the migrations group, producing Migrated.
func Module ¶
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 ¶
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 ¶
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 ¶
ProvideHooks provides a constructor whose result joins the hooks group. The constructor may take any dependencies; its result must be HookSet.
func ProvideMiddleware ¶
ProvideMiddleware provides a constructor whose result joins the middleware group. The constructor may take any dependencies; its result must be MiddlewareSet.
func ProvideMigrations ¶
ProvideMigrations provides a constructor whose result joins the migrations group. The constructor may take any dependencies; its result must be MigrationSet.
func ProvideOperations ¶
ProvideOperations provides a constructor whose result joins the operations group. The constructor may take any dependencies; its result must be OperationSet.
func WithPrincipal ¶
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 ¶
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.