Documentation
¶
Overview ¶
Package store provides universal data access contracts for the Credo framework.
This package defines error sentinels, lifecycle/health interfaces, a connection registry, a registration API, and context-based transaction helpers. It has zero external dependencies — only the Go standard library and Credo's root/fault contracts are imported.
The companion package store/sqldb (a separate Go submodule) wraps *bun.DB with lifecycle management, query builder proxies, error mapping, and transaction support.
Universal Errors ¶
Store errors expose a transport-neutral semantic Kind. The default HTTP policy and future protocol adapters consume that kind independently:
if kind, ok := store.KindOf(err); ok {
// branch on KindNotFound, KindConstraint, KindDeadlock, ...
}
Error retains diagnostic code/constraint/resource metadata and the original cause without exposing them in Credo's default HTTP response. The sentinel HTTPStatus methods remain only as a deprecated compatibility bridge; new application policy should use Kind.
Registration ¶
Use Register to register a data store connection in the DI container with automatic ping, lifecycle tracking, and health aggregation:
store.Register[*sqldb.DB](app, db)
A value that implements Lifecycle is framework-owned after Register succeeds; the DI container is its sole framework shutdown owner. During one teardown DI makes at most one Shutdown attempt if the still-live shutdown context reaches that registration. A deadline exhausted earlier may leave it uncalled. A value that cannot implement Lifecycle may supply its health handle with WithLifecycle only together with WithCallerOwnedLifecycle; in that explicit mode the caller remains responsible for shutdown. Ownership never transfers on a failed registration.
Registration validates the local DI state and reserves the store name, value type, and resource identity before Ping. By default the Lifecycle value itself is the identity; pointer-backed implementations are recommended. Semantic wrappers around another resource implement LifecycleIdentityProvider and return the underlying pointer or another stable token. Embedding a provider promotes that method through ordinary Go method promotion; there is no reflective field scanning. Within one Registry, the same identity cannot be registered under another type or ownership mode; use credo.App.Alias for interface access to an existing registration. Pending reservations are invisible to Registry readers and readiness; the health entry becomes visible only after Ping and DI publication both succeed. The successful value binding and validated Registry binding are protected against credo.App.Replace so DI cannot diverge from lifecycle/readiness state. A composition-root Registry is adopted with the expected-value compare-and-protect form of credo.App.ProtectBinding: if the resolved pointer changed before protection, registration fails without protecting the replacement. Registry exposes Registry.HealthAll as a read-only view and has no public mutation API.
Identity uniqueness covers only resources registered through Register. Publishing the same Lifecycle again through raw credo.App.Provide, credo.App.ProvideFactory, credo.App.ProvideValue, credo.App.ProvideProtectedValue, or credo.App.Replace is unsupported and can create contradictory ownership or multiple shutdown attempts. In particular, a caller-owned lifecycle handle must not also be registered in DI as a Shutdowner.
Registered stores contribute stable readiness probes. Named and store checks run in parallel with enforced per-check deadlines and panic isolation. Health.Cause carries typed diagnostics for logging while remaining excluded from JSON; free-form Details values are never interpreted as error causes. Overlapping readiness requests share one in-flight execution per store, so a cancellation-ignoring probe cannot accumulate a goroutine per request.
Context-Based Transactions ¶
A custom adapter creates one typed TxScope per logical connection. The transaction type is fixed at construction, preventing a concrete/interface mismatch from silently selecting the fallback connection:
scope := store.NewTxScope[Client]() ctx = scope.WithTx(ctx, tx) conn := scope.Conn(ctx, base) tx, err := scope.RequireTx(ctx) // no fallback
The companion store/sqldb adapter owns a private typed scope per DB. Its proxy terminals participate automatically; db.Conn(ctx) is the transaction-aware Bun escape hatch. The standalone WithTx, GetTx, and Conn helpers remain only as deprecated compatibility APIs.
Index ¶
- Constants
- Variables
- func Conn[T any](ctx context.Context, fallback T) Tdeprecated
- func ConnInScope[T any](ctx context.Context, scope *TxScope[T], fallback T) T
- func GetTx[T any](ctx context.Context) (T, bool)deprecated
- func GetTxInScope[T any](ctx context.Context, scope *TxScope[T]) (T, bool)
- func IsTransient(err error) bool
- func Register[R any](app *credo.App, value R, opts ...RegisterOption) error
- func RequireTxInScope[T any](ctx context.Context, scope *TxScope[T]) (T, error)
- func WithTx[T any](ctx context.Context, tx T) context.Contextdeprecated
- func WithTxInScope[T any](ctx context.Context, scope *TxScope[T], tx T) context.Context
- func Wrap(kind error, cause error) error
- type Error
- type Health
- type HealthStatus
- type Kind
- type Lifecycle
- type LifecycleIdentityProvider
- type RegisterOption
- type Registry
- type TxScope
Constants ¶
const DefaultPingTimeout = 5 * time.Second
DefaultPingTimeout is the default context deadline for the initial health check performed by Register. Lifecycle.Ping implementations must honor the context; Register does not detach a non-cooperative Ping call.
Variables ¶
var ( // ErrNotFound indicates that the requested record does not exist. ErrNotFound error = newKindError(KindNotFound, "store: record not found") // ErrAlreadyExists indicates a unique or primary-key violation. ErrAlreadyExists error = newKindError(KindAlreadyExists, "store: duplicate record") // ErrDuplicate is the compatibility name for [ErrAlreadyExists]. // // Deprecated: use ErrAlreadyExists or inspect [KindAlreadyExists]. ErrDuplicate = ErrAlreadyExists // ErrConstraint indicates a persistent integrity constraint violation other // than a duplicate, such as foreign-key, not-null, or check failure. ErrConstraint error = newKindError(KindConstraint, "store: constraint violation") // ErrSerialization indicates that transaction serialization failed. ErrSerialization error = newKindError(KindSerialization, "store: serialization failure") // ErrDeadlock indicates that the database detected a deadlock. ErrDeadlock error = newKindError(KindDeadlock, "store: deadlock") // ErrContention indicates a transient lock/busy conflict whose safe retry // scope is not implied by this sentinel. ErrContention error = newKindError(KindContention, "store: contention") // ErrConflict is the legacy umbrella for constraint and concurrency errors. // New classifiers return a more specific sentinel whose errors.Is method // still matches ErrConflict. // // Deprecated: inspect ErrConstraint, ErrSerialization, ErrDeadlock, or // ErrContention instead. ErrConflict error = newKindError(KindConflict, "store: conflict") // ErrTimeout indicates that a database operation exceeded a verified // deadline or statement timeout. ErrTimeout error = newKindError(KindTimeout, "store: timeout") // temporarily unavailable. ErrUnavailable error = newKindError(KindUnavailable, "store: unavailable") // ErrReadOnly indicates that a write was attempted against a read-only // transaction or server. ErrReadOnly error = newKindError(KindReadOnly, "store: read-only") )
Sentinel errors for data access operations.
var ErrTxMissing = errors.New("store: transaction missing from context")
ErrTxMissing indicates that a transaction-required operation was called without a transaction in its context scope.
Functions ¶
func Conn
deprecated
Conn returns the transaction from context if present, otherwise returns the fallback connection. Repositories call this in every method for opt-in TX participation.
Deprecated: create a typed TxScope with NewTxScope and use its Conn method. A standalone type-keyed helper cannot isolate multiple logical connections that share T or prevent concrete/interface mismatches.
func ConnInScope ¶
ConnInScope returns the transaction from context for the given scope if present, otherwise returns the fallback connection. Panics if scope is nil.
func GetTx
deprecated
GetTx retrieves a transaction handle from the context. Returns the zero value and false if no TX of type T is stored.
Deprecated: create a typed TxScope with NewTxScope and use its GetTx method. A standalone type-keyed helper cannot prevent concrete/interface type mismatches across call sites.
func GetTxInScope ¶
GetTxInScope retrieves a scoped transaction handle from the context. Returns the zero value and false if no TX of type T is stored for the scope. Panics if scope is nil.
func IsTransient ¶
IsTransient reports whether the primary store classification describes a condition that may clear. It never means an operation, transaction callback, or externally visible side effect is safe to retry.
func Register ¶
func Register[R any](app *credo.App, value R, opts ...RegisterOption) error
Register registers value as type R in the DI container, pings the connection, and tracks it in the Registry for lifecycle and health management.
If value implements Lifecycle, it is used directly for Ping, Health, and Shutdown. Otherwise, a separate health handle is accepted only through WithLifecycle plus the explicit WithCallerOwnedLifecycle opt-out.
Steps:
- Validate name, lifecycle ownership, and predictable DI conflicts
- Resolve or create Registry and wire the internal readiness seam
- Privately reserve the unique store name, DI type, and lifecycle identity
- Ping the connection with a finite timeout
- Publish the DI value and Registry health entry together
- Emit validated, secret-free registration warning codes after publication
Shutdown ownership is unambiguous. A direct Lifecycle value is framework-owned after successful registration. The DI container visits it in reverse registration order and makes at most one shutdown attempt if the live shutdown deadline reaches its entry. A separate WithLifecycle handle remains caller-owned and requires WithCallerOwnedLifecycle; the caller must arrange its shutdown. The Registry never closes connections.
On every error, including Ping or final DI publication failure, Register exposes no health entry and does not acquire ownership. This does not undo an independent raw DI publication performed by the caller or another goroutine. Do not place the same lifecycle in DI through Provide, ProvideFactory, ProvideValue, ProvideProtectedValue, or Replace and also through Register; register it once and use credo.App.Alias for additional interface views.
func RequireTxInScope ¶
RequireTxInScope retrieves a scoped transaction handle or returns ErrTxMissing when the scope has no transaction in ctx. Panics if scope is nil.
func WithTx
deprecated
WithTx stores a transaction handle in the context. T is the transaction type (e.g., bun.Tx, bun.IDB). Panics if tx is nil, including a typed-nil pointer stored in an interface T.
Deprecated: create a typed TxScope with NewTxScope and use its WithTx method. A standalone type-keyed helper cannot prevent a producer and consumer from choosing different concrete/interface forms of the same transaction type.
func WithTxInScope ¶
WithTxInScope stores a transaction handle in the context for a specific logical connection scope. Panics if scope is nil — scopes are created once at wiring time via NewTxScope, so a nil scope is a programming error. Panics if tx is nil, including a typed-nil pointer stored in an interface T.
Types ¶
type Error ¶
type Error struct {
Kind Kind
Op string
Resource string
Constraint string
Code string
Transient bool
Cause error
}
Error is a structured, transport-neutral data-access error.
Code, Constraint, Resource, and Cause are internal diagnostic metadata. The default HTTP renderer classifies only Kind and never serializes these fields. Transient describes whether the condition may clear; it does not guarantee that replaying the operation or transaction is safe.
func (*Error) HTTPStatus
deprecated
type Health ¶
type Health struct {
// Status is the current health state.
Status HealthStatus
// Latency is the round-trip time of the health check probe.
Latency time.Duration
// Cause is the typed internal reason for a down or degraded result.
// It is excluded from JSON so unauthenticated readiness endpoints cannot
// leak connection targets or driver diagnostics. Credo logs it and exposes
// its text only when HealthConfig.ExposeErrors is explicitly enabled.
Cause error `json:"-"`
// Details holds adapter-specific information such as database version,
// connection pool statistics, or replication lag.
Details map[string]any
}
Health is the result of a connection health check.
type HealthStatus ¶
type HealthStatus string
HealthStatus represents the health state of a data store connection.
const ( // StatusUp indicates the connection is healthy. StatusUp HealthStatus = "UP" // StatusDown indicates the connection is unreachable. StatusDown HealthStatus = "DOWN" // StatusDegraded indicates the connection is available but impaired. StatusDegraded HealthStatus = "DEGRADED" )
type Kind ¶
Kind is the transport-neutral semantic category of a store error.
const ( KindUnknown Kind = fault.KindUnknown KindNotFound Kind = fault.KindNotFound KindAlreadyExists Kind = fault.KindAlreadyExists KindConstraint Kind = fault.KindConstraint KindSerialization Kind = fault.KindSerialization KindDeadlock Kind = fault.KindDeadlock KindContention Kind = fault.KindContention KindConflict Kind = fault.KindConflict KindTimeout Kind = fault.KindTimeout KindReadOnly Kind = fault.KindReadOnly )
Store error kinds. They are aliases of the shared transport-neutral kinds so Credo's HTTP policy and future gRPC policy can classify them without importing store and creating a package cycle.
type Lifecycle ¶
type Lifecycle interface {
// Ping verifies the connection is alive and must honor ctx cancellation;
// Register invokes it synchronously with a deadline.
Ping(ctx context.Context) error
// Shutdown gracefully closes the connection.
// Implementations should respect ctx.Done() for timely cleanup.
Shutdown(ctx context.Context) error
// Health returns structured health information including status,
// latency, and adapter-specific details (pool stats, version, etc.).
Health(ctx context.Context) Health
}
Lifecycle manages connection health and shutdown for a data store. Adapters (e.g., store/sqldb) implement this interface for use with Register. Implementations should normally be pointer-backed so Register can retain a stable physical-resource identity and reject duplicate ownership.
type LifecycleIdentityProvider ¶
LifecycleIdentityProvider is an optional Lifecycle extension for semantic wrappers that represent another physical resource. ResourceIdentity must return a non-nil, comparable, reflexively equal token that remains stable for the resource lifetime; the underlying resource pointer is the usual token. Wrapper types that embed an implementation inherit this method automatically.
type RegisterOption ¶
type RegisterOption func(*registerOptions)
RegisterOption configures a Register call.
func WithCallerOwnedLifecycle ¶
func WithCallerOwnedLifecycle() RegisterOption
WithCallerOwnedLifecycle explicitly keeps lifecycle shutdown ownership with the caller when WithLifecycle is used for a value that cannot itself implement Lifecycle. The caller must arrange shutdown, for example through credo.App.OnShutdown. The option is invalid without WithLifecycle.
func WithLifecycle ¶
func WithLifecycle(lc Lifecycle) RegisterOption
WithLifecycle provides the handle used for Ping and Health when value does not implement Lifecycle itself. It must be paired with WithCallerOwnedLifecycle: the caller retains responsibility for invoking Shutdown on that separate handle. Prefer making value implement Lifecycle so the framework can own shutdown.
func WithName ¶
func WithName(name string) RegisterOption
WithName sets the stable identifier used in health reporting. It rejects an empty or padded name, control characters, and the reserved "credo." prefix. If omitted, Register uses the pointer-unwrapped, package-qualified name of R.
func WithPingTimeout ¶
func WithPingTimeout(d time.Duration) RegisterOption
WithPingTimeout overrides the default Ping context deadline (5s) for the initial health check performed by Register.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry tracks data store connections for startup ping and health aggregation. It is created automatically on the first Register call and stored in the DI container.
Registry is a read-only public facade; Register is its only mutation path. It never closes connections. The DI container owns direct Lifecycle values and makes at most one shutdown attempt per value if the live shutdown deadline reaches its entry. Explicitly caller-owned lifecycle handles remain the caller's responsibility.
type TxScope ¶
type TxScope[T any] struct { // contains filtered or unexported fields }
TxScope isolates transactions that share the same Go type but belong to different logical connections. T is fixed when the scope is created, so a concrete transaction cannot be stored and then silently missed by reading the same scope through a different interface type.
The marker field keeps TxScope non-zero-size on purpose: in Go, pointers to distinct zero-size structs may share an address, which would let two independent scopes collide as context keys. A non-empty field guarantees a unique address per NewTxScope call.
func NewTxScope ¶
NewTxScope creates a unique transaction scope whose transaction type is T.
func (*TxScope[T]) Conn ¶
Conn returns this scope's transaction from the context if present, otherwise the fallback connection — the call repositories make for opt-in scoped TX participation. It is the method form of ConnInScope.
func (*TxScope[T]) GetTx ¶
GetTx retrieves this scope's transaction handle from the context, returning the zero value and false when none is stored. It is the method form of GetTxInScope.
func (*TxScope[T]) RequireTx ¶
RequireTx retrieves this scope's transaction handle or returns ErrTxMissing when none is stored. It never falls back to a base connection. It is the method form of RequireTxInScope.