Documentation
¶
Index ¶
- Constants
- Variables
- func AddComputedField[T any](s *Server, modelName, name string, ...) error
- func Batch(ctx *ServerContext, fn func(*Batcher) error) error
- func Bind[T any](ctx *ServerContext) (*T, error)
- func Create[T any](ctx *ServerContext, record *T) (*T, error)
- func DecodeCursor(token string) (value any, id string, err error)
- func Delete[T any](ctx *ServerContext, id string) error
- func EncodeCursor(value any, id string) string
- func ExtraColumns(record any) map[string]any
- func For[T any](ctx *ServerContext) (*T, bool)
- func FormatDuration(d time.Duration) string
- func List[T any](ctx *ServerContext, q *QueryParams) ([]*T, error)
- func MapToRecord(model *ModelMeta, m map[string]any) (any, error)
- func Migrate(s *Server, migrations []Migration) error
- func Mount(r chi.Router, services ...*Server)
- func PanicRecoverer(logger *slog.Logger) func(http.Handler) http.Handler
- func PresentColumns(record any) map[string]struct{}
- func RandomString(length int, charset string) string
- func Read[T any](ctx *ServerContext, id string) (*T, error)
- func RecordToMap(model *ModelMeta, record any) map[string]any
- func ResolveManyToManyForTest(reg *Registry) error
- func RotateEncryptionKey(ctx context.Context, s *Server, modelName, oldKeyID, newKeyID string) (rotated int, err error)
- func SetExtra(record any, m map[string]any)
- func SetPresentColumns(record any, cols []string)
- func SplitCSV(s string) []string
- func Update[T any](ctx *ServerContext, id string, record *T) (*T, error)
- type APIError
- type APIResponse
- type ActionConfig
- type ActionHandlerFunc
- type ActionOpenAPI
- type AggregateField
- type AggregateOp
- type AggregateQuery
- type AsyncAPIChannel
- type AsyncAPIComponents
- type AsyncAPIConfig
- type AsyncAPIInfo
- type AsyncAPIMessage
- type AsyncAPIMessageRef
- type AsyncAPIOperation
- type AsyncAPIServer
- type AsyncAPIServerConfig
- type AsyncAPISpec
- type AuthIdentityType
- type AuthInfo
- type BaseModel
- type Batcher
- func (b *Batcher) Create(model string, data map[string]any) (map[string]any, error)
- func (b *Batcher) Delete(model string, id string) error
- func (b *Batcher) List(model string, q *QueryParams) ([]map[string]any, error)
- func (b *Batcher) Model(name string) *ModelAccessor
- func (b *Batcher) Read(model string, id string) (map[string]any, error)
- func (b *Batcher) Update(model string, id string, data map[string]any) (map[string]any, error)
- type CacheStore
- type ComputedField
- type ComputedFunc
- type Config
- type ConstraintKind
- type CursorParams
- type DBAdapter
- type DriverType
- type ErrConstraint
- type EventDoc
- type ExportFormat
- type FieldMeta
- type FieldTags
- type FileACLMode
- type FileMeta
- type FileStorage
- type FilterExpr
- type FilterOperator
- type GlobalSearchConfig
- type HavingClause
- type IndexSpec
- type KeyProvider
- type ListResult
- type LocaleMode
- type LocaleOptions
- type LocaleString
- type LockCondition
- type LockScopeSpec
- type MemoryCache
- type MiddlewareConfig
- type MiddlewareFunc
- type MiddlewareOption
- type Migration
- type ModelAccessor
- func (a *ModelAccessor) Create(data map[string]any) (map[string]any, error)
- func (a *ModelAccessor) Delete(id string) error
- func (a *ModelAccessor) List(q *QueryParams) ([]map[string]any, error)
- func (a *ModelAccessor) Read(id string) (map[string]any, error)
- func (a *ModelAccessor) Update(id string, data map[string]any) (map[string]any, error)
- type ModelConfig
- type ModelMeta
- func (m *ModelMeta) EncryptedFields() []FieldMeta
- func (m *ModelMeta) FieldByDBName(name string) *FieldMeta
- func (m *ModelMeta) FieldByJSONName(name string) *FieldMeta
- func (m *ModelMeta) FileFields() []FieldMeta
- func (m *ModelMeta) FilterableDBNames() map[string]bool
- func (m *ModelMeta) HasEncryptedFields() bool
- func (m *ModelMeta) HasFileFields() bool
- func (m *ModelMeta) HasScheduled() bool
- func (m *ModelMeta) RelationByKey(key string) *RelationMeta
- func (m *ModelMeta) RelationByModel(name string) *RelationMeta
- func (m *ModelMeta) ResolveAdapter(global DBAdapter) DBAdapter
- func (m *ModelMeta) Scheduled() []ScheduledSpec
- type ModelMiddleware
- type OASEncoding
- type OASMediaType
- type OASOperation
- type OASParameter
- type OASRequestBody
- type OASResponse
- type OASSchema
- type OASSecurityScheme
- type OnDeleteAction
- type OpenAPIComponents
- type OpenAPIContext
- type OpenAPIInfo
- type OpenAPIMiddlewareFunc
- type OpenAPIPipeline
- type OpenAPIServer
- type OpenAPISpec
- type OpenAPIStepRegistry
- type OpenAPITag
- type Operation
- type PathItem
- type Pinger
- type Pipeline
- type PipelineTrace
- type Position
- type QueryParams
- type RawResult
- type RecursiveDirection
- type RecursiveQuery
- type Registry
- type RegistryAccessor
- type RelationKind
- type RelationMeta
- type RequestBody
- type ResponseMeta
- type Row
- type SQLTyper
- type SchedAction
- type ScheduledSpec
- type SearchOptions
- type SearchResult
- type Server
- func (c *Server) Action(cfg ActionConfig)
- func (s *Server) AddComputedField(modelName, name string, fn ComputedFunc) error
- func (c *Server) AddService(s Service)
- func (c *Server) DB() DBAdapter
- func (c *Server) EnableGlobalSearch(cfg ...GlobalSearchConfig)
- func (c *Server) Go(fn func(context.Context))
- func (c *Server) Handler() http.Handler
- func (c *Server) MigrateOnly(ctx context.Context) error
- func (s *Server) MustAddComputedField(modelName, name string, fn ComputedFunc)
- func (c *Server) MustRegister(args ...any)
- func (c *Server) PathPrefix() string
- func (c *Server) RealtimeDoc(cfg AsyncAPIConfig)
- func (c *Server) Register(args ...any) error
- func (c *Server) Registry() RegistryAccessor
- func (c *Server) SetDB(db DBAdapter)
- func (c *Server) SetKeyProvider(kp KeyProvider)
- func (c *Server) SetStorage(fs FileStorage)
- func (c *Server) Shutdown(ctx context.Context) error
- func (c *Server) Start() error
- func (c *Server) StartWithContext(ctx context.Context) error
- type ServerContext
- func (c *ServerContext) Abort(statusCode int, code, message string)
- func (c *ServerContext) Aggregate(modelName string, agg AggregateQuery) ([]Row, error)
- func (c *ServerContext) BeginTx(ctx context.Context, opts *TxOptions) (Tx, error)
- func (c *ServerContext) BindJSON(v any) error
- func (c *ServerContext) DeleteField(jsonName string)
- func (c *ServerContext) DriverType() DriverType
- func (c *ServerContext) Field(jsonName string) (any, bool)
- func (c *ServerContext) Get(key string) (any, bool)
- func (c *ServerContext) GetModel(modelName string) *ModelAccessor
- func (c *ServerContext) GoBackground(fn func(context.Context))
- func (c *ServerContext) HasRole(role string) bool
- func (c *ServerContext) LockForUpdate(modelName, id string) (map[string]any, error)
- func (c *ServerContext) Logger() *slog.Logger
- func (c *ServerContext) QueryModel(modelName string, q *QueryParams) ([]map[string]any, error)deprecated
- func (c *ServerContext) QueryParam(name string) string
- func (c *ServerContext) RawExec(query string, args ...any) (int64, error)
- func (c *ServerContext) RawQuery(query string, args ...any) ([]Row, error)
- func (c *ServerContext) RecursiveQuery(modelName string, q RecursiveQuery) ([]Row, error)
- func (c *ServerContext) Search(opts SearchOptions) ([]SearchResult, error)
- func (c *ServerContext) ServiceName() string
- func (c *ServerContext) Set(key string, val any)
- func (c *ServerContext) SetField(jsonName string, value any)
- func (c *ServerContext) TrackStoredFile(key string, storage FileStorage)
- func (c *ServerContext) TryBindJSON(v any) (ok bool, err error)
- func (c *ServerContext) URLParam(name string) string
- type Service
- type ServiceFunc
- type SoftDeletable
- type SoftDeleteConfig
- type SoftDeleteStyle
- type SortDir
- type SortExpr
- type StepRegistry
- type Tx
- type TxOptions
- type UploadedFile
- type WithDeletedAt
- type WithIsDeleted
Constants ¶
const ALPHANUM = UPPER + LOWER + DIGITS
const DIGITS = "0123456789"
const DefaultMaxExportRows = 100_000
DefaultMaxExportRows is the row cap used when ModelConfig.MaxExportRows is unset.
const LOWER = "abcdefghijklmnopqrstuvwxyz"
const LOWER_D = LOWER + DIGITS
const SingletonID = "singleton"
SingletonID is the fixed primary key of the single row backing a ModelConfig.Singleton model. The row is created with this id on first access and every GET/PATCH on the model addresses it.
const TABLE_NAME_PREFIX = ""
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const UPPER_D = UPPER + DIGITS
Variables ¶
var ErrFileNotFound = errors.New("file not found")
ErrFileNotFound is returned by FileStorage.Retrieve when the requested key does not exist.
var ErrNoAdapter = errNoAdapter("maniflex: no database adapter configured")
ErrNoAdapter is returned by BeginTx when no database adapter has been configured on the Server instance.
var ErrNotFound = errors.New("record not found")
ErrNotFound is returned by adapter methods when a requested record does not exist.
Functions ¶
func AddComputedField ¶
func AddComputedField[T any](s *Server, modelName, name string, fn func(ctx context.Context, record *T) (any, error)) error
AddComputedField registers a typed derived field: the callback receives the loaded record as a *T instead of a JSON map. It's the typed counterpart of (*Server).AddComputedField (typed-models migration, T4.5 / locked assumption §5). On read and list the record is the scanned *T; on create/update echo it is bridged best-effort from the stored row.
maniflex.AddComputedField(srv, "Product", "stock_level",
func(ctx context.Context, p *Product) (any, error) {
return stockService.CurrentLevel(ctx, p.ID)
})
func Batch ¶
func Batch(ctx *ServerContext, fn func(*Batcher) error) error
Batch executes fn inside a single database transaction shared by all Batcher operations. On success the transaction commits; on any error it rolls back.
Transaction ownership:
- If ctx.Tx is nil, Batch opens a new transaction, commits on success, rolls back on fn error or ctx.Abort, and restores ctx.Tx to nil on exit.
- If ctx.Tx is already set (e.g. from WithTransaction or a manual BeginTx), Batch joins it. It does not commit or roll back — the outer owner is responsible for lifecycle. fn returning an error still propagates up so the outer owner can react.
ctx.Tx and ctx.Ctx are updated to point at the batch transaction for the duration of fn, so any code inside fn that calls ctx.RawQuery, ctx.LockForUpdate, or ctx.GetModel also participates in the same transaction. Both fields are restored when Batch returns.
err := maniflex.Batch(ctx, func(b *maniflex.Batcher) error {
inv, err := b.Create("Invoice", invoiceData)
if err != nil { return err }
for _, line := range lines {
line["invoice_id"] = inv["id"]
if _, err := b.Create("InvoiceLine", line); err != nil { return err }
}
return nil
})
func Bind ¶
func Bind[T any](ctx *ServerContext) (*T, error)
Bind is For with an error instead of a bool, for handlers that require a typed body and want to return early on its absence.
func Create ¶
func Create[T any](ctx *ServerContext, record *T) (*T, error)
Create inserts record (a *T) and returns the stored representation, scanned back into a field-populated *T.
func DecodeCursor ¶
DecodeCursor reverses EncodeCursor. JSON numbers are decoded as int64 when integral (else float64) so an integer cursor column compares as a number on both Postgres and SQLite rather than arriving as a lossy float.
func Delete ¶
func Delete[T any](ctx *ServerContext, id string) error
Delete removes (or soft-deletes) the T identified by id.
func EncodeCursor ¶
EncodeCursor builds the opaque token that points just past (value, id) in the keyset ordering. It is base64url(JSON) so it survives in a query string and carries no separator-collision risk.
func ExtraColumns ¶
ExtraColumns returns the framework-internal extra columns staged on a record carrier (NULLs, driver-shaped scalars, *_hmac, populated includes), or nil. DB adapters call it from their write builders to append these columns. Transitional Phase-3 bridge; removed in Phase 7.
func For ¶
func For[T any](ctx *ServerContext) (*T, bool)
For returns the request's typed record (*T) and true when one is bound to ctx.Record, or (nil, false) otherwise — e.g. a read with no body, a model whose body failed to decode, or a T that doesn't match the bound record.
func FormatDuration ¶
func List ¶
func List[T any](ctx *ServerContext, q *QueryParams) ([]*T, error)
List returns a page of T records. q may be nil (page 1, default limit).
func MapToRecord ¶
MapToRecord is the exported transition bridge used by DB adapters to wrap a DB-column-keyed map in a *T carrier. Transitional Phase-3 bridge; removed in Phase 7.
func Migrate ¶
Migrate runs the supplied migrations against the server's DB adapter, recording each applied version in a schema_migrations table.
Migrations are sorted by Version and executed in order. A migration is skipped if its version already appears in schema_migrations, so Migrate is safe to call on every startup. Each migration runs inside its own transaction (unless NoTransaction is set); the version row is inserted in the same transaction so a crash mid-migration leaves the table in either the pre- or post-state, never partially applied.
Migrate is independent of AutoMigrate: AutoMigrate handles struct-driven CREATE/ALTER for green-field development, while Migrate is the path for data backfills, concurrent index builds, and coordinated multi-step deployments. Most production setups call AutoMigrate first (to ensure tables exist) and then Migrate (for the explicit, ordered changes).
if err := maniflex.Migrate(server, []maniflex.Migration{
{Version: "0001_seed_roles", Up: seedRoles},
{Version: "0002_backfill_slugs", Up: backfillSlugs},
}); err != nil {
log.Fatal(err)
}
func Mount ¶
Mount registers each Server instance on r at its configured PathPrefix. It lets multiple services share a single chi router so that shared middleware (logging, tracing, CORS) is applied once:
r := chi.NewRouter()
r.Use(myLoggingMiddleware, myTracingMiddleware)
maniflex.Mount(r,
ordersService, // PathPrefix: /api/orders
inventoryService, // PathPrefix: /api/inventory
ledgerService, // PathPrefix: /api/ledger
)
http.ListenAndServe(":8080", r)
func PanicRecoverer ¶
PanicRecoverer returns an HTTP middleware that catches panics, emits a structured slog log record, and writes a JSON error response consistent with the rest of the maniflex API — instead of the plain-text HTML page that chi's built-in Recoverer returns.
Every panic produces:
A slog record at ERROR level with the fields: - method, path (request context) - request_id (from chi's RequestID middleware, when present) - panic (the recovered value as a string) - stack (full goroutine stack trace as a single string)
An HTTP 500 response with body: {"error": {"code": "PANIC", "message": "internal server error"}}
The stack trace is intentionally omitted from the HTTP response — it is available in the log and must not be leaked to API clients.
logger may be nil; when nil slog.Default() is used.
func PresentColumns ¶
PresentColumns returns the set of DB column names the bridge recorded as present on a record carrier, or nil when none were recorded. Transitional Phase-3 bridge; removed in Phase 7.
func RandomString ¶
func Read ¶
func Read[T any](ctx *ServerContext, id string) (*T, error)
Read returns the single T identified by id, or maniflex.ErrNotFound.
func RecordToMap ¶
RecordToMap is the exported transition bridge used by DB adapters to convert a returned *T back into the map the still-map pipeline consumes. Transitional Phase-3 bridge; removed in Phase 7.
func ResolveManyToManyForTest ¶
ResolveManyToManyForTest runs the M2M resolution pass. Exported for use in tests.
func RotateEncryptionKey ¶
func RotateEncryptionKey(ctx context.Context, s *Server, modelName, oldKeyID, newKeyID string) (rotated int, err error)
RotateEncryptionKey re-encrypts every value of a model's encrypted fields from oldKeyID to newKeyID, one page of records at a time. Both keys must be active in the KeyProvider until rotation is confirmed complete.
The operation is not atomic across all rows: if interrupted, rows encrypted with oldKeyID and rows encrypted with newKeyID will coexist. Both keys must remain available until the function returns without error.
For large tables (millions of rows), run this as a background job (3C.3) rather than inline at startup.
Pagination uses keyset (cursor) traversal — `WHERE id > $last ORDER BY id` — so each page covers fresh rows even as Update mutates the table and the underlying adapter's natural order is unstable (e.g. Postgres without an explicit ORDER BY). Offset pagination over a mutating set silently skipped or revisited rows.
func SetExtra ¶
SetExtra merges columns into a record carrier's extra map (populated includes, computed fields, _through). No-op for records without a BaseModel carrier.
func SetPresentColumns ¶
SetPresentColumns records, on a record carrier, the set of DB columns that were actually populated (e.g. the columns scanStruct read for a ?select= projection). The serializer emits only present columns. No-op for records without a BaseModel carrier.
func SplitCSV ¶
SplitCSV splits a comma-separated string, trimming whitespace from each part. Exported so DB adapters in sub-packages can use it.
func Update ¶
func Update[T any](ctx *ServerContext, id string, record *T) (*T, error)
Update writes record (a *T) over the row identified by id. It is a full-record update: every column (except id) is written. For partial/PATCH writes use the pipeline (HTTP PATCH) or ctx.GetModel(name).Update with a present map.
Types ¶
type APIError ¶
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
}
APIError is the error body for non-2xx responses.
type APIResponse ¶
type APIResponse struct {
StatusCode int
Data any
Error *APIError
Meta *ResponseMeta
AsIs bool
// Dir, when non-empty, adds {"_dir": Dir} to the response meta. For list
// responses Meta already carries Dir via ResponseMeta.Dir; this field is
// used for single-record responses (read/create/update) that have no
// pagination meta — it produces {"data": ..., "meta": {"_dir": "rtl"}}.
Dir string
}
APIResponse is the structured response written to the wire by the Response pipeline step.
func (*APIResponse) Write ¶
func (r *APIResponse) Write(w http.ResponseWriter)
Write serialises the APIResponse and sends it to the HTTP ResponseWriter.
type ActionConfig ¶
type ActionConfig struct {
// Routing — path must use chi v5 {param} syntax, not :param.
Method string // HTTP method: "GET", "POST", "PATCH", "PUT", "DELETE"
Path string // e.g. "/appointments/{id}/cancel"
// Behaviour
Handler ActionHandlerFunc // required
Middleware []MiddlewareFunc // run between global Auth and Handler; nil is fine
// ResponseMiddleware runs after Handler but before the Response step writes
// the response — the per-action counterpart to Pipeline.Response. Use it to
// post-process ctx.Response (headers, envelope shaping) for one action
// without registering against the synthetic model name. nil is fine.
ResponseMiddleware []MiddlewareFunc
// OpenAPI metadata (all optional)
Tags []string // defaults to ["Actions"] when empty
Summary string
Deprecated bool
// OpenAPI schema hints (all optional)
//
// RequestBody describes the expected request body for POST/PUT/PATCH actions.
// Use maniflex.JSONRequestBody(schema) to build one from an *OASSchema.
//
// Responses maps HTTP status codes to response body schemas.
// A nil schema means no response body (suitable for 204 No Content).
// When set, these replace the default generated responses entirely.
//
// Example:
// Responses: map[int]*maniflex.OASSchema{
// 201: {Type: "object", Properties: map[string]*maniflex.OASSchema{"id": {Type: "string"}}},
// 404: nil,
// },
RequestBody *OASRequestBody
Responses map[int]*OASSchema
// OpenAPI carries richer, optional OpenAPI metadata for this action,
// most notably request/response schemas inferred from Go structs. It
// complements the inline RequestBody / Responses fields above, which take
// precedence when both are set. See ActionOpenAPI.
OpenAPI ActionOpenAPI
}
ActionConfig configures one custom action endpoint.
type ActionHandlerFunc ¶
type ActionHandlerFunc func(ctx *ServerContext) error
ActionHandlerFunc is the signature for custom action handler functions. The handler must either set ctx.Response or call ctx.Abort before returning nil. If neither is done, the Response step defaults to 200 OK with no body.
type ActionOpenAPI ¶
type ActionOpenAPI struct {
// RequestSchema is a Go struct value, pointer, or reflect.Type whose
// exported fields (read via json + mfx tags) become an application/json
// request body schema. Ignored when ActionConfig.RequestBody is set.
RequestSchema any
// ResponseSchema is a Go struct value, pointer, or reflect.Type reflected
// into the success-response body schema. Ignored when ActionConfig.Responses
// is set.
ResponseSchema any
// ResponseStatus is the HTTP status the ResponseSchema documents.
// Defaults to 200 when zero.
ResponseStatus int
// Description is a long-form operation description (Markdown). Summary on
// ActionConfig remains the short one-liner.
Description string
// QueryParams declares query-string parameters in addition to the path
// parameters extracted automatically from the route.
QueryParams []OASParameter
// Security lists security requirement objects for the operation, e.g.
// []map[string][]string{{"bearerAuth": {}}}. The referenced schemes must
// be registered separately (see middleware/openapi.AddSecurityScheme).
Security []map[string][]string
}
ActionOpenAPI holds optional OpenAPI documentation for a custom action that goes beyond the inline ActionConfig.RequestBody / Responses fields. Every field is optional and the zero value adds nothing to the spec.
The headline feature is schema inference: set RequestSchema / ResponseSchema to a Go struct value (or pointer) annotated with the same json + mfx tags as models, and the OpenAPI generator reflects it into an application/json schema — no hand-written *OASSchema required.
server.Action(maniflex.ActionConfig{
Method: "POST",
Path: "/appointments/{id}/reschedule",
OpenAPI: maniflex.ActionOpenAPI{
RequestSchema: RescheduleRequest{}, // {new_time string `mfx:"required"`}
ResponseSchema: Appointment{},
ResponseStatus: 200,
QueryParams: []maniflex.OASParameter{{
Name: "notify", In: "query",
Schema: &maniflex.OASSchema{Type: "boolean"},
}},
Security: []map[string][]string{{"bearerAuth": {}}},
},
Handler: rescheduleHandler,
})
type AggregateField ¶
type AggregateField struct {
Op AggregateOp
Field string
As string
}
AggregateField selects one aggregate to compute. Field is the DB column to aggregate over; leave it empty for AggCount to mean COUNT(*). As overrides the result column alias; defaults to "<op>_<field>" (or "count" for COUNT(*)).
type AggregateOp ¶
type AggregateOp string
AggregateOp is the SQL aggregate function applied to a column.
const ( AggCount AggregateOp = "count" AggCountDistinct AggregateOp = "count_distinct" AggSum AggregateOp = "sum" AggAvg AggregateOp = "avg" AggMin AggregateOp = "min" AggMax AggregateOp = "max" )
type AggregateQuery ¶
type AggregateQuery struct {
Select []AggregateField
GroupBy []string
Where []*FilterExpr
Having []HavingClause
OrderBy []SortExpr
Limit int
}
AggregateQuery is the input to ctx.Aggregate. All slices are optional except Select. GroupBy referenced columns must be DB column names on the model; HAVING aliases must match an entry in Select; OrderBy may reference either an aggregate alias or a GroupBy column.
type AsyncAPIChannel ¶
type AsyncAPIChannel struct {
Description string `json:"description,omitempty"`
Subscribe *AsyncAPIOperation `json:"subscribe,omitempty"`
Publish *AsyncAPIOperation `json:"publish,omitempty"`
}
AsyncAPIChannel is a Channel Item Object. Each channel is one event type. The application sends, so events the client receives carry a `subscribe` operation (AsyncAPI 2.x perspective is the application's).
type AsyncAPIComponents ¶
type AsyncAPIComponents struct {
Messages map[string]*AsyncAPIMessage `json:"messages,omitempty"`
Schemas map[string]*OASSchema `json:"schemas,omitempty"`
}
AsyncAPIComponents holds reusable messages and schemas.
type AsyncAPIConfig ¶
type AsyncAPIConfig struct {
Title string
Version string
Servers []AsyncAPIServerConfig
// Events declares custom event types and their payloads.
Events []EventDoc
// AutoModelEvents derives <model>.created|updated|deleted channels from the
// model registry, using each model's struct as the message payload. This
// mirrors the default events.Emit type naming; apps that customise
// EmitConfig.Type should declare their events explicitly via Events instead.
AutoModelEvents bool
}
AsyncAPIConfig is registered once via Server.RealtimeDoc to enable the {PathPrefix}/asyncapi.json endpoint. Title/Version default to the API name.
type AsyncAPIInfo ¶
type AsyncAPIInfo struct {
Title string `json:"title"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
}
AsyncAPIInfo is the Info Object.
type AsyncAPIMessage ¶
type AsyncAPIMessage struct {
Name string `json:"name,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
ContentType string `json:"contentType,omitempty"`
Payload *OASSchema `json:"payload,omitempty"`
}
AsyncAPIMessage is a Message Object. The payload reuses OASSchema because AsyncAPI 2.6 message payloads are JSON-Schema-shaped (the default schema format is a JSON Schema superset), so the same reflection that builds OpenAPI schemas serialises unchanged here.
type AsyncAPIMessageRef ¶
type AsyncAPIMessageRef struct {
Ref string `json:"$ref,omitempty"`
}
AsyncAPIMessageRef references a reusable message in components.messages.
type AsyncAPIOperation ¶
type AsyncAPIOperation struct {
OperationID string `json:"operationId,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Message *AsyncAPIMessageRef `json:"message,omitempty"`
}
AsyncAPIOperation is an Operation Object.
type AsyncAPIServer ¶
type AsyncAPIServer struct {
URL string `json:"url"`
Protocol string `json:"protocol"`
Description string `json:"description,omitempty"`
}
AsyncAPIServer is a Server Object (AsyncAPI 2.6 shape: url + protocol).
type AsyncAPIServerConfig ¶
type AsyncAPIServerConfig struct {
Name string
URL string // e.g. "ws://localhost:8080/ws" or "http://localhost:8080/sse"
Protocol string // "ws", "wss", "sse", "http"
Description string
}
AsyncAPIServerConfig describes one endpoint the realtime hub is mounted at. Name keys the server in the document (defaults to Protocol when empty).
type AsyncAPISpec ¶
type AsyncAPISpec struct {
AsyncAPI string `json:"asyncapi"`
Info AsyncAPIInfo `json:"info"`
Servers map[string]AsyncAPIServer `json:"servers,omitempty"`
Channels map[string]AsyncAPIChannel `json:"channels"`
Components AsyncAPIComponents `json:"components,omitempty"`
}
AsyncAPISpec is the root AsyncAPI 2.6.0 document describing the realtime event channels a client can subscribe to over the WebSocket / SSE hub. It is the event-stream analogue of OpenAPISpec: clients codegen typed event payloads from it the same way they codegen REST types from /openapi.json.
func GenerateAsyncAPI ¶
func GenerateAsyncAPI(reg RegistryAccessor, cfg *Config, asyncCfg AsyncAPIConfig) *AsyncAPISpec
GenerateAsyncAPI builds an AsyncAPI 2.6 document from the model registry and the supplied AsyncAPIConfig. It is called by the asyncapi.json handler and can be called directly to obtain the base document for customisation.
type AuthIdentityType ¶
type AuthIdentityType string
AuthIdentityType classifies the principal behind an authenticated request.
const ( // IdentityAnonymous is the zero value — the request is unauthenticated or // the identity type was not set by the auth middleware. IdentityAnonymous AuthIdentityType = "" // IdentityHuman represents an interactive end-user (employee, patient, admin). IdentityHuman AuthIdentityType = "human" // IdentityServiceAccount represents a machine caller (background job, // service-to-service, API integration). IdentityServiceAccount AuthIdentityType = "service_account" )
type AuthInfo ¶
type AuthInfo struct {
// Core identity — present in every auth scheme.
UserID string
Roles []string
Claims map[string]any // raw token claims or equivalent; always non-nil when Auth is set
// Multi-tenancy — the partition key for the authenticated principal.
// Set by JWTAuth when JWTOptions.TenantClaim is configured, or directly
// by any auth middleware (subdomain tenancy, X-Tenant-ID header, etc.).
TenantID string
// IdentityType classifies the caller. Zero value (IdentityAnonymous) means
// the type was not determined; treat it as a human for backward compatibility.
IdentityType AuthIdentityType
// Scopes holds the OAuth2 scopes granted to the token.
// Populated by JWTAuth from the configured ScopesClaim (default "scope").
Scopes []string
// SessionID is the JWT "jti" claim or session token ID. Empty for API-key
// callers. Useful for read audit trails that need to record which session
// accessed a record.
SessionID string
// AuthMethod describes how the caller authenticated.
// JWTAuth sets this to "jwt"; APIKeyAuth sets it to "api_key".
// Conventional values: "jwt", "api_key", "session", "oauth2".
AuthMethod string
}
AuthInfo carries identity information set by the Auth pipeline step. Populate it inside your auth middleware via ctx.Auth = &maniflex.AuthInfo{...}
type BaseModel ¶
type BaseModel struct {
ID string `json:"id" db:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at" mfx:"readonly,sortable"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at" mfx:"readonly,sortable"`
// contains filtered or unexported fields
}
BaseModel provides the standard id / created_at / updated_at columns. Embed it in every model struct you register, else registering model fails `CreatedAt` and `UpdatedAt` are auto-injected. If edited here, make sure the names are edited in the `injectTimestamp` function
type Batcher ¶
type Batcher struct {
// contains filtered or unexported fields
}
Batcher provides transactional CRUD access to registered models inside a maniflex.Batch call. Do not construct directly — obtain via maniflex.Batch.
func (*Batcher) Create ¶
Create inserts a new record for model and returns the stored representation.
func (*Batcher) Delete ¶
Delete removes (or soft-deletes) the record identified by id from model. Returns maniflex.ErrNotFound when absent.
func (*Batcher) List ¶
List returns a page of records from model matching q. q may be nil (defaults to page 1, limit 20, no filters or sorts).
func (*Batcher) Model ¶
func (b *Batcher) Model(name string) *ModelAccessor
Model returns a *ModelAccessor for modelName bound to the batch transaction. All five CRUD operations on the accessor participate in the same transaction. The accessor records an error on first use when modelName is not registered, or when modelName routes to a different adapter than the batch transaction (a single transaction cannot span databases).
type CacheStore ¶
type CacheStore interface {
Get(ctx context.Context, key string) (any, bool)
Set(ctx context.Context, key string, value any, ttl time.Duration)
Delete(ctx context.Context, key string)
}
CacheStore is a generic, TTL-aware key/value cache shared by middleware that needs cross-request memoisation (idempotency keys, rate-limit windows, per-user response caches, etc.). Implementations must be safe for concurrent use.
Get returns ok=false when no entry exists for the key or it has expired. Set persists value under key for at most ttl. Delete removes the entry under key; it is a no-op when the key is absent.
type ComputedField ¶
type ComputedField struct {
Name string
Fn ComputedFunc
// contains filtered or unexported fields
}
ComputedField is one registered virtual field on a model. Name is the JSON key it appears under in responses; collisions with real model fields are rejected at registration. Computed fields cannot be filtered or sorted — they're materialised only on the way out.
type ComputedFunc ¶
ComputedFunc derives a virtual field value from the loaded row. It runs in the Response step after the DB row has been converted to JSON keys, so the `row` map keys are JSON field names.
A non-nil error is logged and the field is omitted from the response — a computed-field failure must not poison the whole record.
type Config ¶
type Config struct {
// Port the HTTP server listens on. Default: 8080.
Port int
// PathPrefix is prepended to every generated route. Default: "/api".
PathPrefix string
// StaticDir is the filesystem directory served as static files. A relative
// path is resolved against the working directory. When empty it defaults to
// "<cwd>/static". If the directory does not exist it is skipped with a
// warning (no error). Ignored when StaticDisabled is true.
StaticDir string
// StaticPrefix is the URL path prefix under which StaticDir is served.
// Default: "/static". Unlike model routes it is mounted at the router root,
// NOT under PathPrefix, preserving the historical "<cwd>/static → /static"
// mapping.
StaticPrefix string
// StaticDisabled turns off static file serving entirely, even when the
// directory exists. Use it when "./static" is present for other reasons and
// must not be exposed over HTTP.
StaticDisabled bool
// ServiceName identifies this service in logs, audit records, and outgoing
// requests. When set:
// - every framework log line gains a "service" attribute,
// - every audit record's ServiceName field is populated,
// - the X-Service-Name response header is set on every response.
// When empty (the default) none of the above happens — zero behavioural
// change for callers that don't configure it.
ServiceName string
// DB is the database adapter to use. Required before calling Start().
DB DBAdapter
// AutoMigrate runs schema creation/migration on Start(). Default: true.
AutoMigrate bool
// ShutdownTimeout is the maximum duration Start() waits for in-flight
// requests to complete after a shutdown signal is received before forcing
// connections closed. Default: 30s.
//
// Set a shorter value for fast-cycling environments (e.g. tests, lambdas),
// and a longer value when requests may take several seconds (e.g. bulk
// imports, large file uploads).
ShutdownTimeout time.Duration
// Logger is the slog.Logger used for all framework-level log output:
// server lifecycle messages (startup, shutdown, migration), per-request
// logs emitted via ServerContext.Logger(), and DB adapter messages such as
// AutoMigrate column-drift warnings.
//
// When nil, slog.Default() is used, which writes plain-text lines to
// stderr. Set an explicit logger to route output to a JSON handler, a
// remote aggregator, or a test capture handler.
Logger *slog.Logger
// PanicLogger is the slog.Logger used by the panic recovery middleware.
// Every unhandled panic is logged at ERROR level with structured fields:
// method, path, request_id, panic value, and full stack trace.
//
// When nil, Logger is used (falling back to slog.Default() if Logger is
// also nil). Set PanicLogger explicitly only when panics must be routed
// to a different sink than the rest of the framework logs.
PanicLogger *slog.Logger
// Trace configures pipeline tracing for debug purposes.
// Set Trace.Enabled to true to activate all standard trace output (step
// enter/exit, timings, and abort call sites). Individual sub-flags allow
// finer control — see PipelineTrace for details.
//
// All trace output is emitted at DEBUG level through Config.Logger, so it
// is invisible unless the logger's handler accepts DEBUG records.
Trace PipelineTrace
// QueryTimeout is the maximum duration allowed for a single request's
// database operations. When non-zero a context.WithTimeout deadline is
// attached to ServerContext.Ctx before the pipeline runs, so every DB adapter
// call made during the request — including calls from middleware — inherits
// the same deadline.
//
// If a query exceeds this deadline the DB step returns HTTP 504 with error
// code "TIMEOUT" instead of the usual 500.
//
// Zero (the default) means no per-request timeout is applied; ctx.Ctx
// carries the HTTP request context as-is, which has no deadline unless the
// client disconnects.
//
// Typical values:
// 5s — tight APIs where every response must be fast
// 30s — general OLTP; catches runaway queries without affecting normal use
// 0 — no timeout (legacy / migration path)
QueryTimeout time.Duration
// HealthCheckDB controls whether GET /health pings the database.
//
// When false (the default) the endpoint always returns {"status":"ok"} with
// HTTP 200 — matching the previous behaviour and suitable for liveness
// probes that only need to know the process is alive.
//
// When true the handler calls db.Ping() with a HealthTimeout deadline:
// - On success: HTTP 200 {"status":"ok", "db":"ok"}
// - On failure: HTTP 503 {"status":"degraded","db":"error","error":"..."}
//
// Enable this for Kubernetes readiness probes so the pod is only marked
// ready once it can actually reach the database.
HealthCheckDB bool
// DBWriteURL is the DSN/connection-string for the primary (write) database.
// Not used by the framework directly — set Config.DB with the adapter you
// open from this URL. Populated by ConfigFromEnv.
DBWriteURL string
// DBReadURL is the DSN/connection-string for the read replica. Pass an empty
// string to route reads to the write primary. Populated by ConfigFromEnv.
DBReadURL string
// FileStorage is the storage backend for file uploads. When nil, models
// with mfx:"file" fields will reject multipart file uploads with 501.
// Set before calling Start(), or use SetStorage() for two-step init.
//
// Use storage.NewLocalStorage(path) for disk-based storage, or implement
// the FileStorage interface for S3, R2, GCS, etc.
FileStorage FileStorage
// FileSignedURLTTL is the default time-to-live for pre-signed URLs generated
// for mfx:"file_acl:signed" fields. Default: DefaultFileSignedURLTTL (1 hour).
FileSignedURLTTL time.Duration
// FileMiddleware wraps the standalone /files endpoints (POST /files,
// GET /files/*, DELETE /files/*) with the supplied pipeline middleware
// chain. Middlewares run in slice order; any that sets ctx.Response
// short-circuits the request before the file handler runs.
//
// Empty (the default) leaves /files unauthenticated — backward-compatible
// with pre-fix behaviour but unsafe for production: anyone who guesses a
// key can delete arbitrary files. Recommended production setup:
//
// maniflex.Config{
// FileStorage: storage.NewLocalStorage("./uploads"),
// FileMiddleware: []maniflex.MiddlewareFunc{
// auth.JWTAuth("...", auth.JWTOptions{}),
// auth.RequireRole("admin"),
// },
// }
//
// Per-model attachment routes (mfx:"file" fields on a registered model)
// already run through the full Auth / DB pipeline and are unaffected.
FileMiddleware []MiddlewareFunc
// KeyProvider is the encryption key provider for mfx:"encrypted" fields.
// When nil, any attempt to create or update a record with encrypted fields
// will be rejected with HTTP 500 ENCRYPTION_NOT_CONFIGURED.
// Reads of already-encrypted values return the raw stored ciphertext string.
//
// Use pkg/encryption.EnvKeyProvider for keys in environment variables, or
// pkg/encryption.VaultKeyProvider for HashiCorp Vault Transit.
KeyProvider KeyProvider
// HealthTimeout is the maximum time the health handler waits for the
// database ping to complete. Only used when HealthCheckDB is true.
// Default: 3s.
//
// Choose a value smaller than your probe's timeoutSeconds so the handler
// can return a clean 503 before the probe itself times out:
//
// readinessProbe:
// httpGet:
// path: /api/health
// timeoutSeconds: 5 # probe timeout
// # → set HealthTimeout to 3s so 503 arrives before 5s probe timeout
HealthTimeout time.Duration
// OnStart is a lightweight lifecycle hook run once during boot, after
// migration and DB-ready but before the HTTP listener opens — the same slot
// as Service.Start, and ahead of any registered services. A non-nil error
// aborts boot exactly like a failed migration. The ctx is cancelled when
// shutdown begins.
//
// Use it for callers that want a start hook without defining a Service type;
// for components that also need an ordered Stop, register a Service instead.
OnStart func(ctx context.Context) error
// OnShutdown is the symmetric hook run once during graceful shutdown, after
// the HTTP listener has drained and all services have stopped. The ctx is a
// fresh deadline context bounded by ShutdownTimeout. A returned error is
// logged but does not change the shutdown outcome.
OnShutdown func(ctx context.Context) error
}
Config is the top-level configuration passed to New().
func ConfigFromEnv ¶
ConfigFromEnv builds a Config by reading standard environment variables.
If prefix is non-empty (e.g. "ORDERS"), each variable is prefixed with an underscore separator: ORDERS_PORT, ORDERS_DB_WRITE_URL, etc. If prefix is empty the variables are read without a prefix: PORT, DB_WRITE_URL, etc.
Variables read:
PREFIX_PORT → Config.Port (integer) PREFIX_DB_WRITE_URL → Config.DBWriteURL (string) PREFIX_DB_READ_URL → Config.DBReadURL (string) PREFIX_QUERY_TIMEOUT_MS → Config.QueryTimeout (milliseconds) PREFIX_SHUTDOWN_TIMEOUT_S → Config.ShutdownTimeout (seconds) PREFIX_SERVICE_NAME → Config.ServiceName (string) PREFIX_HEALTH_CHECK_DB → Config.HealthCheckDB (bool: "true"/"1"/"yes")
Unset or empty variables are silently ignored; Config.ApplyDefaults() is NOT called — callers may customise further before passing to maniflex.New().
func (*Config) ApplyDefaults ¶
func (c *Config) ApplyDefaults()
type ConstraintKind ¶ added in v0.1.2
type ConstraintKind string
ConstraintKind classifies a database constraint violation so the DB step can map it to the right HTTP status: unique / foreign-key → 409 Conflict, and not-null → 422 Validation. The zero value ("") is treated as a generic conflict, preserving the original behaviour for normalisers that don't set it.
const ( ConstraintUnique ConstraintKind = "unique" ConstraintForeignKey ConstraintKind = "foreign_key" ConstraintNotNull ConstraintKind = "not_null" )
type CursorParams ¶
type CursorParams struct {
// Field is the DB column the cursor walks (resolved from the model).
Field string
// Direction is the keyset walk direction; the id tiebreaker uses the same
// direction so the ORDER BY and the boundary predicate stay consistent.
Direction SortDir
// HasToken is false on the first page (?cursor= with an empty value) and
// true once a token from a previous page is supplied. When false the bound
// predicate is omitted and the walk starts from the first row.
HasToken bool
AfterValue any // decoded cursor field value of the last row of the previous page
AfterID string // decoded id of the last row of the previous page (tiebreaker)
// NextCursor is the token to fetch the next page, set by the adapter from the
// last returned row. Empty when the page was the last one (HasMore == false).
NextCursor string
// HasMore reports whether at least one more row exists after this page.
HasMore bool
}
CursorParams carries the state for one keyset (cursor) paginated list request. It is set on QueryParams.Cursor by ParseQueryParams when the request opts into cursor mode via ?cursor= on a model that declares a cursor field (mfx:"cursor_field:..." or ModelConfig.CursorField).
Keyset pagination orders by (cursor field, id) so the boundary is total even when the cursor field has ties, then walks the dataset with a WHERE bound instead of OFFSET — so it never skips or duplicates rows when rows are inserted or deleted between page fetches.
The Field/Direction/After* members are inputs filled from the request; the DB adapter writes NextCursor/HasMore back as outputs after fetching the page.
type DBAdapter ¶
type DBAdapter interface {
// AutoMigrate creates or updates tables/collections for the given models.
// Called once on Server.Start() when Config.AutoMigrate is true.
AutoMigrate(ctx context.Context, reg RegistryAccessor) error
// BeginTx starts a database transaction with the given options.
// Pass nil opts for the default isolation level.
// The returned Tx must be committed or rolled back by the caller.
BeginTx(ctx context.Context, opts *TxOptions) (Tx, error)
// Run arbitrary SQL
Raw(ctx context.Context, query string, args ...any) RawResult
// FindByID returns the record (*T) matching id.
// Includes listed in q.Includes are populated as nested objects.
// Returns ErrNotFound when the record does not exist.
FindByID(ctx context.Context, model *ModelMeta, id string, q *QueryParams) (any, error)
// FindByIDForUpdate fetches the record (*T) and acquires a pessimistic
// row-level lock. Postgres: appends FOR UPDATE; use inside a transaction so
// the lock is held until commit/rollback. SQLite: behaves like FindByID —
// the lock is acquired at the transaction level (BEGIN IMMEDIATE).
// Returns ErrNotFound when the record does not exist.
FindByIDForUpdate(ctx context.Context, model *ModelMeta, id string) (any, error)
// FindMany returns a page of records ([]any of *T) and the total count
// before pagination.
FindMany(ctx context.Context, model *ModelMeta, q *QueryParams) ([]any, int64, error)
// Create inserts the record (*T). The adapter generates the ID if absent.
// Returns the stored representation (*T) including generated fields.
// Returns *ErrConstraint on unique/check violations.
Create(ctx context.Context, model *ModelMeta, record any) (any, error)
// Update applies a patch to the record identified by id. record is a *T
// carrying the new values; present names the DB columns to write (PATCH
// semantics) — only those (plus updated_at) are updated. Returns the updated
// representation (*T). Returns ErrNotFound when absent, *ErrConstraint on
// unique/check violations.
Update(ctx context.Context, model *ModelMeta, id string, record any, present map[string]struct{}) (any, error)
// Delete removes or soft-deletes the record identified by id.
// Returns ErrNotFound when absent.
Delete(ctx context.Context, model *ModelMeta, id string) error
// Close releases any resources held by the adapter.
Close() error
}
DBAdapter is the interface all database backends must implement. Methods receive a *ModelMeta describing the target model and a *QueryParams carrying pagination, filters, sorts, and relation includes.
type DriverType ¶
type DriverType int
DriverType distinguishes SQL dialects used by DBAdapter implementations.
const ( Postgres DriverType = iota SQLite )
type ErrConstraint ¶
type ErrConstraint struct {
// Kind classifies the violation (unique, foreign_key, not_null). Empty for
// normalisers that predate the field; treated as a generic conflict.
Kind ConstraintKind
// Table is the DB table name where the violation occurred.
Table string
// Column is the column name that was violated, if the driver exposes it.
// May be empty for drivers that do not provide column-level detail.
Column string
// Detail is the raw driver error message, for logging context.
Detail string
}
ErrConstraint is returned by adapter methods when a database constraint is violated. It is driver-neutral: SQLite "UNIQUE constraint failed: table.column" / "NOT NULL constraint failed: table.column" and Postgres error codes (23505, 23502, 23503) are normalised into this type before being returned to the DB step, which converts it to a 409 Conflict (unique/FK) or a 422 Validation error (not-null).
func (*ErrConstraint) Error ¶
func (e *ErrConstraint) Error() string
type EventDoc ¶
type EventDoc struct {
Type string // CloudEvents type / topic, e.g. "invoice.created"
Title string
Description string
Payload any
}
EventDoc documents one realtime event type's payload for AsyncAPI generation. Payload accepts a struct value, pointer, or reflect.Type annotated with the same json + mfx tags as models; it is reflected into a JSON schema exactly like an action's RequestSchema/ResponseSchema (10.8).
type ExportFormat ¶
type ExportFormat string
ExportFormat selects the wire format the export endpoint produces.
const ( ExportFormatCSV ExportFormat = "csv" ExportFormatXLSX ExportFormat = "xlsx" )
type FieldMeta ¶
type FieldMeta struct {
Name string // Go struct field name
Type reflect.Type // Go type
Tags FieldTags // parsed mfx/json/db tags
Index []int // reflect index path (supports embedded structs)
}
FieldMeta describes one scalar (non-relation) DB column field.
type FieldTags ¶
type FieldTags struct {
// mfx tag directives
Required bool // must be present in create requests
Readonly bool // stripped from all write operations
Immutable bool // settable on create, rejected on update
Filterable bool // may be used in ?filter= queries
Sortable bool // may be used in ?sort= queries
Hidden bool // excluded from all API responses
WriteOnly bool // accepted on write, excluded from responses (e.g. password)
Unique bool // hint to DB adapter to add UNIQUE constraint
Index bool // mfx:"index" → CREATE INDEX on the column in AutoMigrate
Searchable bool // included in full-text search (if supported)
Enum []string // allowed values, e.g. mfx:"enum:draft|published"
Min *float64 // numeric minimum
Max *float64 // numeric maximum
Default string // automatically cast to corresponding type (if possible)
// Relation options — set via mfx:"relation:RelationName;onDelete:cascade"
// Relation names the companion struct field that carries the target model type.
// When set this FK is an explicit relation; when empty the legacy ID-suffix
// convention is used instead.
Relation string
OnDelete OnDeleteAction
// NoRelation opts a convention-FK field (one whose name ends in "ID") out of
// the automatic ID-suffix relation, so e.g. `UserID string mfx:"norelation"`
// stays a plain scalar column instead of implying a BelongsTo relation to a
// User model. Has no effect on fields that aren't convention FKs or that carry
// an explicit relation: tag.
NoRelation bool
// Through is set on []SliceFields to declare a many-to-many relation via a
// junction model. Value is the junction model struct name, e.g. "ProductTag".
// mfx:"through:ProductTag"
Through string
// Encryption options — set via mfx:"encrypted" or mfx:"encrypted,key:patient-pii"
// Encrypted marks the field for transparent AES-256-GCM encryption at rest.
// Values are stored as "enc:<base64(envelope)>" and decrypted on read.
// Filtering and sorting on encrypted fields is not supported.
// If Unique is also set, a companion {field}_hmac column enforces uniqueness
// without exposing the plaintext.
Encrypted bool
EncryptionKey string // key name passed to KeyProvider; defaults to "default"
// File upload options — set via mfx:"file,max_size:10MB,accept:image/*"
// File marks this field as a file upload field. The DB column stores the
// storage key (string). When true, multipart form-data is automatically
// accepted for create/update operations on models containing this field.
File bool
// MaxSize is the maximum allowed file size in bytes. Parsed from
// mfx:"max_size:10MB". Zero means no per-field limit.
MaxSize int64
// Accept is a list of allowed MIME type patterns, e.g. ["image/*", "application/pdf"].
// Parsed from mfx:"accept:image/*|application/pdf".
Accept []string
// AutoDelete controls whether the file is automatically deleted from storage
// when the record is hard-deleted or the field value is replaced by an update.
// Default: true when File is set. Set to false with mfx:"auto_delete:false".
AutoDelete bool
// FileACL controls how the field value is presented in API responses.
// Parsed from mfx:"file_acl:private|signed|public". Default: FileACLPrivate.
// FileACLSigned replaces the storage key with a pre-signed URL.
// FileACLPublic replaces the storage key with a permanent public URL.
FileACL FileACLMode
// Locale marks this field as a bilingual storage field. The Go type must
// be maniflex.LocaleString. Stored as TEXT (SQLite) or JSONB (Postgres).
// Response representation is controlled by LocaleMode (default: split).
Locale bool
// LocaleMode overrides the response representation for this specific field.
// When empty the mode is inherited from ModelConfig.DefaultLocaleMode, then
// LocaleOptions.DefaultLocaleMode, then split (framework default).
// Valid values: "split", "resolve", "dynamic".
LocaleMode LocaleMode
// LocaleModeConflict is set when more than one of split/resolve/dynamic
// appears in the same mfx tag. ScanModel rejects such fields at registration.
LocaleModeConflict bool
// LocaleDefault is the per-field fallback locale used when the client did
// not request a specific locale. Only meaningful in resolve/split mode.
// e.g. mfx:"locale,default_locale:ar" makes Arabic the field-level default.
LocaleDefault string
// LockWhen carries conditions parsed from mfx:"lock_when:field=value"
// directives. Multiple occurrences on the same field accumulate. At
// registration these per-field lists are flattened into ModelMeta.LockWhen
// — the conditions reference the record state, not the field they're
// declared on, so the declaration site is incidental.
LockWhen []LockCondition
// LockScope names a registered model whose row must be locked FOR UPDATE
// before a create. Parsed from mfx:"lock_scope:ModelName".
// The field value is used as the referenced row ID.
// Requires an active transaction (maniflex.WithTransaction).
LockScope string
// CursorField opts the model into keyset (cursor) pagination and names the
// column the cursor walks. Parsed from mfx:"cursor_field:created_at". The
// value is the JSON or DB name of the cursor field; ScanModel resolves it to
// a DB column and stores it on ModelMeta.CursorField. Declaring it on any one
// field (typically the embedded BaseModel) enables the model — the value, not
// the declaration site, picks the cursor column.
CursorField string
// Scheduled-operation directive (8.6). Scheduled is the on/off switch; the
// rest is meaningful only when Scheduled is true. The action flags are NOT
// mutually validated here — parseScheduledTag records exactly what was
// written; ScanModel resolves and rejects (see "No implicit action").
Scheduled bool // mfx:"scheduled" present
SchedSoft bool // ;soft-delete
SchedHard bool // ;hard-delete
SchedField string // ;field=
SchedFrom string // ;from=
SchedTo string // ;to=
SchedHasFrom bool // distinguishes ;from= absent vs. from="" given
SchedHasTo bool // distinguishes ;to= absent vs. to="" given
SchedBadOpt string // first unrecognised option, "" if none — ScanModel errors on it
// Derived names
JSONName string
DBName string
OmitEmpty bool
Ignore bool // mfx:"-" or json:"-"
}
FieldTags holds every directive that can appear in a `mfx:"..."` struct tag, plus the derived JSON and DB column names.
type FileACLMode ¶
type FileACLMode string
FileACLMode controls how file field values are presented in API responses.
const ( // FileACLPrivate is the default: the storage key is returned as-is. // Downloads go through the per-model attachment route (3B.3a) which // enforces the same auth as the parent record read. FileACLPrivate FileACLMode = "private" // FileACLSigned causes the Response step to replace the storage key with // a pre-signed URL valid for Config.FileSignedURLTTL (default 1 hour). // Requires FileStorage.URL() support on the configured backend. FileACLSigned FileACLMode = "signed" // FileACLPublic causes the Response step to replace the storage key with // a permanent public URL. LocalStorage returns a /files/<key> path; // S3Storage returns the object's public URL (requires public-read ACL on // the bucket or object). FileACLPublic FileACLMode = "public" // DefaultFileSignedURLTTL is the default TTL for signed URLs when // Config.FileSignedURLTTL is zero. DefaultFileSignedURLTTL = time.Hour )
type FileMeta ¶
type FileMeta struct {
Key string `json:"key"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
Filename string `json:"filename"`
}
FileMeta describes an uploaded file's metadata.
type FileStorage ¶
type FileStorage interface {
// Store writes the contents of r to the given key with the supplied metadata.
// The key is framework-generated; implementations must create any intermediate
// directories or object prefixes as needed.
Store(ctx context.Context, key string, r io.Reader, meta FileMeta) error
// Retrieve returns a ReadCloser for the file at key, along with its metadata.
// Returns ErrFileNotFound when the key does not exist.
Retrieve(ctx context.Context, key string) (io.ReadCloser, FileMeta, error)
// Delete removes the file at key from storage. Implementations should
// return ErrFileNotFound when the key does not exist so the standalone
// DELETE /files/* handler can translate that into a 404. Returning nil
// for a missing key is also acceptable for backends that cannot detect
// it atomically (notably some S3-compatible APIs); callers must treat
// both as "delete succeeded" except where a 404 is specifically wanted.
Delete(ctx context.Context, key string) error
// Exists reports whether the file at key exists in storage.
Exists(ctx context.Context, key string) (bool, error)
// URL returns a URL suitable for the given access mode.
// For FileACLSigned, ttl is how long the URL remains valid; the backend
// must return an error if it cannot produce time-limited URLs.
// For FileACLPublic, ttl is 0 and a permanent (or very long-lived) URL
// is returned.
// LocalStorage returns a server-relative /files/<key> path for both modes.
// S3Storage returns a presigned GET URL using the AWS SDK presigner.
URL(ctx context.Context, key string, ttl time.Duration) (string, error)
}
FileStorage is the interface all file storage backends must implement. It is analogous to DBAdapter for database operations.
Implementations:
- storage.LocalStorage — disk-based, ships with maniflex
- Bring your own: S3, R2, GCS, etc.
type FilterExpr ¶
type FilterExpr struct {
// Flat filter (not nested)
Field string // DB column name on the primary table
Operator FilterOperator
Value any // raw string from URL; adapters cast as needed
// Nested filter (Field contains a "." and references a BelongsTo relation)
IsNested bool
RelationKey string // e.g. "author"
RelationModel string // e.g. "Author"
RelationTable string // DB table of related model, e.g. "users"
RelationFK string // FK column on THIS table, e.g. "author_id"
NestedField string // DB column on the related table, e.g. "status"
// Locale filter (Field contains a "." and the left side is a locale field)
// e.g. ?filter=name.ar:ilike:قلب
// SQL: name->>'ar' ILIKE ? (Postgres) / json_extract(name,'$.ar') LIKE ? (SQLite)
IsLocale bool // true when filtering on a locale sub-key
LocaleKey string // the locale key portion, e.g. "ar"
// Group is the OR-group index.
//
// Group <= 0 (which includes the struct's zero value) means ungrouped: the
// filter forms its own AND clause. Filters sharing the same Group >= 1 are
// OR-ed together. The zero value is ungrouped on purpose so a hand-built
// FilterExpr AND-s by default — building one with the bare Field/Operator/
// Value set is the common case and must not silently OR.
//
// The URL bracket syntax ?filter[N]=... (N >= 0) is mapped onto Group N+1
// during parsing, so e.g. ?filter[0]=a&filter[0]=b is one OR group; the
// external contract is unchanged.
Group int
}
FilterExpr is a single parsed and validated filter condition.
func ParseFilterParam ¶
func ParseFilterParam(raw string, model *ModelMeta, reg RegistryAccessor) (*FilterExpr, error)
ParseFilterParam parses one filter query parameter value into a FilterExpr.
Format: field:operator[:value]
Examples:
status:eq:published created_at:gte:2024-01-01 author.status:neq:banned (nested — author must be a registered relation) deleted_at:is_null (no value) role:in:admin,editor
type FilterOperator ¶
type FilterOperator string
FilterOperator is a comparison operator used in filter expressions.
const ( OpEq FilterOperator = "eq" // field = value OpNeq FilterOperator = "neq" // field != value OpGt FilterOperator = "gt" // field > value OpGte FilterOperator = "gte" // field >= value OpLt FilterOperator = "lt" // field < value OpLte FilterOperator = "lte" // field <= value OpLike FilterOperator = "like" // field LIKE value (case-sensitive) OpILike FilterOperator = "ilike" // field ILIKE value (case-insensitive) OpIn FilterOperator = "in" // field IN (v1,v2,...) OpNotIn FilterOperator = "not_in" // field NOT IN (v1,v2,...) OpIsNull FilterOperator = "is_null" // field IS NULL (no value) OpNotNull FilterOperator = "not_null" // field IS NOT NULL (no value) OpBetween FilterOperator = "between" // field BETWEEN lo AND hi (value "lo,hi") )
type GlobalSearchConfig ¶
type GlobalSearchConfig struct {
// Path is the route mounted under PathPrefix. Defaults to "/search".
Path string
// DefaultLimit is the merged-result cap when the request omits ?limit=.
// Defaults to 20.
DefaultLimit int
// MaxLimit clamps ?limit= so a client cannot request an unbounded scan.
// Defaults to 100; set negative to disable the clamp.
MaxLimit int
}
GlobalSearchConfig configures the built-in cross-model search endpoint. The zero value is valid — every field falls back to a default.
type HavingClause ¶
type HavingClause struct {
Alias string
Operator FilterOperator
Value any
}
HavingClause filters aggregated results by an aggregate alias. Operator is the same set used by FilterExpr (eq, gt, gte, lt, lte, neq); set ops, like, and null checks are rejected since they make no sense on a numeric aggregate result.
type IndexSpec ¶
type IndexSpec struct {
// Name is the unique index identifier, e.g. "idx_invoice_history_record_id".
Name string
// Columns are the column expressions to index, e.g. ["record_id", "version DESC"].
// Each entry is emitted verbatim inside the ON table(...) clause.
Columns []string
// Unique adds the UNIQUE keyword.
Unique bool
}
IndexSpec describes a database index to create during AutoMigrate.
type KeyProvider ¶
type KeyProvider interface {
// Encrypt encrypts plaintext under keyID and returns a self-describing
// binary envelope that embeds the keyID. The keyID is only needed at write
// time; Decrypt reads it from the envelope automatically.
Encrypt(ctx context.Context, keyID string, plaintext []byte) ([]byte, error)
// Decrypt decrypts an envelope produced by Encrypt. The keyID is extracted
// from the envelope — callers do not need to supply it.
Decrypt(ctx context.Context, envelope []byte) ([]byte, error)
// KeyIDOf extracts the keyID from an envelope without decrypting. Useful
// for audit logging and key rotation checks.
KeyIDOf(envelope []byte) (string, error)
// HMAC returns a deterministic keyed digest of data under keyID. Used to
// enforce UNIQUE constraints on encrypted fields: a companion
// {field}_hmac TEXT UNIQUE column stores this digest so uniqueness can be
// checked without exposing or comparing ciphertexts.
HMAC(ctx context.Context, keyID string, data []byte) ([]byte, error)
}
KeyProvider manages encryption keys for mfx:"encrypted" struct fields.
Values are stored in the database as the string "enc:<base64(envelope)>". The "enc:" prefix lets the framework distinguish already-encrypted values from unencrypted legacy data, so tables can be migrated incrementally.
Use pkg/encryption.EnvKeyProvider for environment-variable-backed keys, or pkg/encryption.VaultKeyProvider for HashiCorp Vault Transit.
type ListResult ¶
type ListResult struct {
Items []any
Total int64
Query *QueryParams
}
ListResult is returned by DBAdapter.FindMany.
As of the typed-models migration (Phase 3) Items holds the type-erased record carriers: each element is a *T for the queried model's GoType. The pipeline bridges them to maps during the transition; Phase 4 consumes them directly.
type LocaleMode ¶
type LocaleMode string
LocaleMode controls how a LocaleString field is represented in responses.
const ( // LocaleModeSplit is the default: the field name holds the resolved string // and an auto-generated companion field (e.g. "name_i18n") always holds the // full locale map. Clients get a stable string type for display while still // having access to all translations. LocaleModeSplit LocaleMode = "split" // LocaleModeResolve always returns the field as a string. The locale is // determined by: ?locale= param → Accept-Language header → field // default_locale → model DefaultLocale → app Default → "en". LocaleModeResolve LocaleMode = "resolve" // LocaleModeDynamic replicates the legacy behavior: the field is a string // when ?locale= is present, the full map otherwise. Opt-in only; not // recommended for new models because the field type is non-deterministic. LocaleModeDynamic LocaleMode = "dynamic" )
type LocaleOptions ¶
type LocaleOptions struct {
// Supported is the list of locale codes the application accepts.
// Requests with a locale outside this list fall through to Default.
// Empty means all locales are accepted as-is.
Supported []string
// Default is the locale used when the request carries no recognisable
// locale preference. Defaults to "en" when empty.
Default string
// FromHeader enables Accept-Language header parsing. When true the
// resolver picks the first value in Supported that matches a language
// tag in the header (ignoring quality values for simplicity).
FromHeader bool
// RTL is the list of locale codes that use right-to-left script.
// When the resolved locale is in this list the response meta gets
// "_dir": "rtl".
RTL []string
// DefaultLocaleMode sets the app-wide default mode for LocaleString fields
// that don't have an explicit mode in their struct tag and whose model
// has no ModelConfig.DefaultLocaleMode. When empty, split is used.
DefaultLocaleMode LocaleMode
// SplitSuffix is the suffix appended to a field name for the i18n companion
// in split mode. Defaults to "_i18n" when empty.
SplitSuffix string
}
LocaleOptions configures the LocaleResolver middleware.
type LocaleString ¶
LocaleString is a map of locale keys to translated strings. Stored as TEXT (SQLite) or JSONB (Postgres) in the database. Use with mfx:"locale" on struct fields:
type Department struct {
maniflex.BaseModel
Name maniflex.LocaleString `mfx:"locale"`
Code string `mfx:"required,unique"`
}
The response representation depends on the field's LocaleMode (default: split):
- split: "name" is the resolved string, "name_i18n" is always the full map
- resolve: "name" is always the resolved string
- dynamic: "name" is a string when ?locale= is set, the full map otherwise
func (LocaleString) MarshalJSON ¶
func (ls LocaleString) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler so LocaleString round-trips cleanly.
func (LocaleString) SQLType ¶
func (LocaleString) SQLType(driver DriverType) string
SQLType implements SQLTyper so the DB adapter maps LocaleString to the correct column type: JSONB in Postgres (enables GIN index on locale keys), TEXT in SQLite.
func (*LocaleString) UnmarshalJSON ¶
func (ls *LocaleString) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler.
type LockCondition ¶
type LockCondition struct {
// JSONName is the JSON field name the condition compares against (the
// directive is declared as `lock_when:status=posted`; JSONName="status").
// At registration this is resolved against the model's JSON names so a
// typo is caught early rather than silently never matching.
JSONName string
// Value is the right-hand side of the equality check. Stored as a string
// because the directive itself is textual; numeric/boolean comparisons go
// through fmt.Sprintf("%v", ...) on the loaded record value to flatten
// type differences.
Value string
}
LockCondition expresses a single mfx:"lock_when:<field>=<value>" directive. When a record's current state matches every Field/Value pair across all conditions attached to the model, the record is "locked" — updates and deletes return 422 RECORD_LOCKED. Each LockCondition contributes one independent rule; if any rule matches, the record is locked.
type LockScopeSpec ¶
type LockScopeSpec struct {
// DBName is the DB column carrying the referenced row's ID.
DBName string
// Model is the registered model name to lock.
Model string
}
LockScopeSpec records one mfx:"lock_scope:ModelName" directive resolved at registration time. The DB step acquires a FOR UPDATE lock on the referenced row before executing a create, preventing write-skew races on shared resources (pharmacy stock, seat inventory, etc.).
Requires an active transaction on the request — register maniflex.WithTransaction(nil) on the Service step for the model.
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is a process-local CacheStore implementation. Suitable for single-instance deployments and tests. For multi-replica deployments back the cache with Redis or another shared store by implementing CacheStore against it.
MemoryCache evicts expired entries lazily on Get and, to bound memory for write-mostly keys (rate-limit windows, idempotency replays) that are never queried again, sweeps the map every memoryCachePruneEvery inserts. The pattern mirrors the rate-limiter (see middleware/db query.go §11B.5): no background goroutine is started, so a test that constructs many caches doesn't leak janitors that the testing harness would have to wait on.
func NewMemoryCache ¶
func NewMemoryCache() *MemoryCache
NewMemoryCache returns a ready-to-use in-process CacheStore.
func (*MemoryCache) Delete ¶
func (m *MemoryCache) Delete(_ context.Context, key string)
Delete implements CacheStore.
type MiddlewareConfig ¶
type MiddlewareConfig struct {
Models []string // restrict to these model names (empty = all)
Operations []Operation // restrict to these operations (empty = all)
Position Position
Name string
}
MiddlewareConfig holds the filter criteria for a registered middleware. An empty Models slice means "all models"; an empty Operations slice means "all operations".
type MiddlewareFunc ¶
type MiddlewareFunc func(ctx *ServerContext, next func() error) error
MiddlewareFunc is the signature every pipeline middleware must satisfy. Call next() to proceed to the next handler in the chain. Return without calling next() to short-circuit the pipeline (e.g. on auth failure).
func Handle ¶
func Handle[T any](fn func(ctx *ServerContext, record *T) error) MiddlewareFunc
Handle adapts a typed handler into a MiddlewareFunc. When a *T record is bound to the request, fn runs with it before next(); when none is bound (e.g. a read operation), fn is skipped and the chain continues. A non-nil error from fn short-circuits the pipeline exactly like any middleware error.
func LocaleResolver ¶
func LocaleResolver(opts LocaleOptions) MiddlewareFunc
LocaleResolver returns a Deserialize-step middleware that determines the active locale for the request and stores it on ctx.Locale.
Resolution order:
- ?locale= query parameter
- Accept-Language header (first match in opts.Supported), when opts.FromHeader is true
- opts.Default (default: "en")
Usage:
server.Pipeline.Deserialize.Register(maniflex.LocaleResolver(maniflex.LocaleOptions{
Supported: []string{"en", "ar"},
Default: "en",
FromHeader: true,
RTL: []string{"ar", "he", "fa", "ur"},
}))
func WithTransaction ¶
func WithTransaction(opts *TxOptions) MiddlewareFunc
WithTransaction wraps the pipeline's DB step in a database transaction. It begins a transaction before the DB step runs and commits it after all After-DB middleware complete. If any step returns an error or sets an error response, the transaction is rolled back instead.
Register it on the Service step (Before position, the default) so it fires just before the DB step:
server.Pipeline.Service.Register(
maniflex.WithTransaction(nil), // nil opts = default isolation
maniflex.ForOperation(maniflex.OpCreate, maniflex.OpUpdate, maniflex.OpDelete),
)
Or on the DB step itself at Replace position to fully replace the default:
server.Pipeline.DB.Register(
maniflex.WithTransaction(nil),
maniflex.AtPosition(maniflex.Replace),
)
Once registered, middleware running in the same request can read ctx.Tx to join the same transaction, or call ctx.BeginTx themselves for nested work.
WithTransaction is safe to use with both Postgres and SQLite. SQLite does not support nested transactions; registering WithTransaction twice for the same request will return an error from the second BeginTx call.
type MiddlewareOption ¶
type MiddlewareOption func(*MiddlewareConfig)
MiddlewareOption is a functional option applied to a MiddlewareConfig.
func AtPosition ¶
func AtPosition(p Position) MiddlewareOption
AtPosition sets where the middleware sits relative to the default handler.
pipeline.Response.Register(addHeaders, maniflex.AtPosition(maniflex.After))
func ForModel ¶
func ForModel(names ...string) MiddlewareOption
ForModel restricts the middleware to the named model(s).
pipeline.Auth.Register(requireLogin, maniflex.ForModel("User", "Post"))
func ForOperation ¶
func ForOperation(ops ...Operation) MiddlewareOption
ForOperation restricts the middleware to the given operation(s).
pipeline.DB.Register(auditLog, maniflex.ForOperation(maniflex.OpCreate, maniflex.OpUpdate))
func WithName ¶
func WithName(name string) MiddlewareOption
WithName sets name for middleware for debug purposes
pipeline.Auth.Register(rateLimit, maniflex.WithName("rate-limiter"))
type Migration ¶
type Migration struct {
// Version uniquely identifies the migration. Required, must be non-empty.
Version string
// Up applies the migration. Required.
Up func(ctx context.Context, tx *sql.Tx) error
// NoTransaction disables the BEGIN/COMMIT wrapper for this migration.
// Use only for DDL that the database refuses to run inside a transaction
// (e.g. Postgres CREATE INDEX CONCURRENTLY). When true, the *sql.Tx
// passed to Up is a thin shim that executes against a dedicated
// connection with no rollback semantics.
NoTransaction bool
}
Migration is a single versioned schema change applied exactly once.
Version is a unique identifier ordered lexicographically — typically a zero-padded sequence ("0001_init", "0002_add_index") or a timestamp ("20260429_120000_backfill"). Migrate sorts the slice by Version before running, so registration order does not matter.
Up runs inside a transaction. Returning an error rolls the transaction back and aborts the migration run; the version is not recorded as applied. Use the supplied *sql.Tx for both DDL and any data backfill so the entire step is atomic.
Note: some DDL statements are not transactional on every backend (e.g. Postgres `CREATE INDEX CONCURRENTLY` cannot run inside a transaction). For those, set NoTransaction: true and run the statement directly against the supplied *sql.Tx — it will be a degenerate "transaction" that simply runs the statement on a dedicated connection without BEGIN/COMMIT wrapping.
type ModelAccessor ¶
type ModelAccessor struct {
// contains filtered or unexported fields
}
ModelAccessor exposes the five standard CRUD operations for a single registered model. Obtain one via ServerContext.GetModel. All methods route through the active transaction (ctx.Tx) when one is set.
func (*ModelAccessor) Create ¶
Create inserts a new record and returns the stored representation. Returns *maniflex.ErrConstraint on unique/check violations.
func (*ModelAccessor) Delete ¶
func (a *ModelAccessor) Delete(id string) error
Delete removes (or soft-deletes) the record identified by id. Returns maniflex.ErrNotFound when absent.
func (*ModelAccessor) List ¶
func (a *ModelAccessor) List(q *QueryParams) ([]map[string]any, error)
List returns a page of records matching q. q may be nil (defaults to page 1, limit 20, no filters or sorts).
func (*ModelAccessor) Read ¶
func (a *ModelAccessor) Read(id string) (map[string]any, error)
Read returns the single record identified by id. Returns maniflex.ErrNotFound when the record does not exist.
type ModelConfig ¶
type ModelConfig struct {
// TableName overrides the auto-generated snake_case plural table name.
TableName string
// SoftDelete opts the model into soft deletion.
SoftDelete SoftDeleteConfig
// Middleware holds per-model pipeline middleware installed at registration
// time. nil means no per-model middleware.
Middleware *ModelMiddleware
// Versioned enables field-change history for this model. AutoMigrate
// creates a sibling {model}_history table. Every write emits a history row
// with a per-field diff. Equivalent to mfx:"versioned" on BaseModel.
Versioned bool
// VersionedDiffOnly skips the snapshot column. Only changed fields are
// stored. Equivalent to mfx:"versioned:diff_only" on BaseModel.
VersionedDiffOnly bool
// Indices declares extra DB indexes to create during AutoMigrate. Use this
// to pre-declare indexes that the framework would otherwise auto-generate
// (e.g. for mfx:"scheduled" timestamp columns) so duplicates are skipped.
Indices []IndexSpec
// Adapter overrides Config.DB for this model only. When non-nil, all CRUD
// reads/writes, AutoMigrate, and transactions for this model route through
// it instead of the global Config.DB.
//
// Leave nil to use Config.DB. Used to spread aggregates across separate
// databases (e.g. orders DB vs. inventory DB) without running multiple
// service binaries.
//
// maniflex.Batch and pkg/saga cannot span adapters in a single transaction —
// Batch construction rejects mixed-adapter model sets; sagas are the
// supported cross-adapter pattern.
Adapter DBAdapter
// ExportEnabled mounts GET /:model/export when true. The route accepts the
// same filter and sort query parameters as the standard list endpoint and
// streams the result as CSV (default) or XLSX (?format=xlsx). Hidden and
// writeonly fields are excluded from the output.
ExportEnabled bool
// MaxExportRows caps the number of rows the export endpoint will return.
// 0 means use DefaultMaxExportRows (100,000). Exports that would exceed
// the cap return 413 Request Entity Too Large.
MaxExportRows int
// CursorField opts the model into keyset (cursor) pagination and names the
// column the ?cursor= walk orders by. Value is the JSON or DB name of the
// field (resolved to a DB column at registration); empty leaves the model on
// offset (?page=/?limit=) pagination only. Equivalent to declaring
// mfx:"cursor_field:<name>" on a field. The cursor field should be indexed
// and effectively monotonic (e.g. created_at, a sequence) for best results;
// id is always appended as the tiebreaker so the keyset boundary is total.
CursorField string
// AggregateEnabled mounts GET /:model/aggregate when true. The route accepts
// a JSON body describing an aggregation (select/group_by/where/having/
// order_by/limit), validates every referenced field against the model's
// filterable/sortable allow-list, and runs it through ctx.Aggregate. It
// dispatches as the list operation, so auth and row-isolation middleware
// registered for OpList apply unchanged and any ?filter= query parameters
// (including tenancy force-filters) are AND-ed into the aggregate WHERE.
//
// Field and operator names use the same convention as ?filter=/?sort=:
// the JSON field name (DB column name also accepted). Only count, sum, avg,
// min, max, and count_distinct are exposed. The result is returned as a
// JSON array of group rows under the usual {"data": ...} envelope.
AggregateEnabled bool
// SearchLanguage names the text-search configuration used for full-text
// search (?q=) on mfx:"searchable" fields. On Postgres it is the
// to_tsvector / websearch_to_tsquery configuration name (default "english");
// on SQLite it is ignored (the FTS5 porter tokenizer is language-agnostic).
// The value is embedded into SQL as a config identifier — it must be a plain
// identifier ([A-Za-z_]+) and is rejected at registration otherwise. Empty
// means the framework default ("english").
SearchLanguage string
// GlobalSearchable opts the model into the built-in cross-model search
// endpoint (GET /search, enabled via Server.EnableGlobalSearch). Only models
// with this flag are searched by that endpoint and may be named in its
// ?models= filter. It requires the model to declare at least one
// mfx:"searchable" field — registration fails otherwise. It has no bearing on
// per-model ?q= search (that needs only mfx:"searchable"), nor on ctx.Search
// called with an explicit model list (the Action-scoped path, which the app
// authorises itself).
GlobalSearchable bool
// DefaultLocale is the model-level fallback locale for LocaleString fields
// in resolve/split mode when the client did not request a specific locale
// and the field has no default_locale tag. Falls back to
// LocaleOptions.Default when empty.
DefaultLocale string
// DefaultLocaleMode sets the default response representation for all
// LocaleString fields on this model that do not carry an explicit mode tag.
// When empty the app-level LocaleOptions.DefaultLocaleMode applies, then
// the framework default (split).
DefaultLocaleMode LocaleMode
// OptimisticLock enables If-Match / ETag concurrency control for PATCH and
// DELETE operations. When set, the DB step fetches the current record before
// executing the write, computes its ETag (MD5 of the JSON response body),
// and returns 412 Precondition Failed if the If-Match header does not match.
//
// The ETag format is identical to the one emitted by response.Cache, so
// clients can obtain it via a preceding GET and use it on the mutating
// request without special handling.
//
// Requests that omit the If-Match header bypass the check — the field
// opts in to enforcement, not to mandatory locking.
OptimisticLock bool
// Singleton turns the model into a single-row config / feature-flag
// resource. Instead of the usual collection + item routes, the model mounts
// only GET and PATCH on its bare table path (no id, no POST/DELETE/list):
//
// GET /{table} → read the one row
// PATCH /{table} → update the one row
//
// The row is provisioned lazily under the well-known SingletonID on first
// access, so GET returns column defaults before anything is written and
// PATCH always targets an existing row. This is the blessed shape for the
// "admin edits one config record, clients read it at launch" pattern (e.g.
// GET /config). Singleton models must not declare mfx:"required" fields —
// the auto-provisioned row has no values to satisfy them.
Singleton bool
// Headless registers the model fully — migration, registry, typed access,
// relations — but mounts NO REST routes for it. Use it to back a path with a
// custom server.Action instead of the auto-generated CRUD: a model and an
// action cannot both own the same method+path (chi panics at boot), so set
// Headless on the model to free its table path (e.g. GET /threads) for the
// action. The model is still reachable via ctx.GetModel / typed CRUD and via
// relations from other models. Takes precedence over Singleton.
Headless bool
}
ModelConfig holds user-supplied options for a single registered model. All fields are optional; sensible defaults are derived from the struct name.
type ModelMeta ¶
type ModelMeta struct {
Name string
GoType reflect.Type
TableName string
Fields []FieldMeta // scalar DB column fields
Relations []RelationMeta // FK and slice-based relations
SoftDelete SoftDeleteConfig
Config ModelConfig
Indices []IndexSpec // extra DB indexes created during AutoMigrate
// LockWhen aggregates all `lock_when:field=value` directives across the
// model's fields. Populated at registration via collectLockWhen. The
// default validate step checks these on Update/Delete: if any condition
// matches the currently-stored record, the request is rejected with 422
// RECORD_LOCKED. Empty slice means the model is never locked.
LockWhen []LockCondition
// LockScopes aggregates all `lock_scope:ModelName` directives across the
// model's fields. Populated at registration via collectLockScopes. The DB
// step acquires a FOR UPDATE lock on each referenced row before executing a
// create. Requires an active transaction. Empty slice means no auto-locking.
LockScopes []LockScopeSpec
// Computed holds per-model virtual fields registered via
// Server.AddComputedField. They are materialised in the Response step
// after toJSONMap and cannot be filtered or sorted — read-only output
// only. Mutated under mu; readers should snapshot the slice header under
// the read lock before iterating to avoid racing with AddComputedField.
Computed []ComputedField
// Adapter overrides the global Config.DB for this model. nil means use
// the global. Copied from ModelConfig.Adapter at registration.
Adapter DBAdapter
// CursorField is the resolved DB column for keyset (cursor) pagination, or
// "" when the model only supports offset pagination. Resolved at registration
// from ModelConfig.CursorField or an mfx:"cursor_field:..." field tag.
CursorField string
// SearchFields are the DB column names of every mfx:"searchable" field, in
// declaration order. Non-empty enables full-text search (?q=) on the model:
// AutoMigrate provisions the driver's native FTS index (Postgres tsvector
// column + GIN, SQLite FTS5 shadow table) over these columns. Resolved at
// registration by collectSearchFields, which also rejects non-string fields.
SearchFields []string
// contains filtered or unexported fields
}
ModelMeta holds all reflection-derived and user-supplied metadata for a registered model. Built once at registration; treated as read-only afterwards.
func ScanModel ¶
func ScanModel(v any, cfg ModelConfig) (*ModelMeta, error)
ScanModel inspects a struct value (or pointer) and returns a populated ModelMeta.
func (*ModelMeta) EncryptedFields ¶
EncryptedFields returns all fields marked mfx:"encrypted".
func (*ModelMeta) FieldByDBName ¶
func (*ModelMeta) FieldByJSONName ¶
func (*ModelMeta) FileFields ¶
FileFields returns all fields with the File tag set.
func (*ModelMeta) FilterableDBNames ¶
func (*ModelMeta) HasEncryptedFields ¶
HasEncryptedFields reports whether the model has any mfx:"encrypted" fields.
func (*ModelMeta) HasFileFields ¶
HasFileFields reports whether the model has any mfx:"file" fields.
func (*ModelMeta) HasScheduled ¶
HasScheduled reports whether the model declares any valid scheduled field.
func (*ModelMeta) RelationByKey ¶
func (m *ModelMeta) RelationByKey(key string) *RelationMeta
func (*ModelMeta) RelationByModel ¶
func (m *ModelMeta) RelationByModel(name string) *RelationMeta
func (*ModelMeta) ResolveAdapter ¶
ResolveAdapter returns the adapter to use for this model: the per-model override if set, otherwise the supplied global. Both may be nil; callers must check.
func (*ModelMeta) Scheduled ¶
func (m *ModelMeta) Scheduled() []ScheduledSpec
Scheduled returns the resolved scheduled-transition specs for the model. A nil/empty slice means there is nothing for a scheduled.Runner to sweep.
type ModelMiddleware ¶
type ModelMiddleware struct {
Auth []MiddlewareFunc
Deserialize []MiddlewareFunc
Validate []MiddlewareFunc
Service []MiddlewareFunc
DB []MiddlewareFunc
Response []MiddlewareFunc
}
ModelMiddleware holds per-step middleware registered alongside a model at registration time. Every middleware is implicitly scoped to the model it is registered with; adding a ForModel option is not required and would be redundant.
Used by jobs/maniflex.Mount to install write-blockers and force-filters without requiring a separate server.Pipeline.X.Register call after registration.
type OASEncoding ¶
type OASEncoding struct {
ContentType string `json:"contentType,omitempty"`
}
OASEncoding is an Encoding Object, used inside multipart/form-data media types to declare per-part content type and other transfer attributes.
type OASMediaType ¶
type OASMediaType struct {
Schema *OASSchema `json:"schema,omitempty"`
Encoding map[string]OASEncoding `json:"encoding,omitempty"`
}
OASMediaType is a Media Type Object.
type OASOperation ¶
type OASOperation struct {
OperationID string `json:"operationId"`
Summary string `json:"summary"`
Description string `json:"description,omitempty"`
Tags []string `json:"tags,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
Parameters []OASParameter `json:"parameters,omitempty"`
RequestBody *OASRequestBody `json:"requestBody,omitempty"`
Responses map[string]OASResponse `json:"responses"`
// Security lists the security requirement objects for this operation.
// Each map names a security scheme (declared in components.securitySchemes)
// mapped to the scopes it requires, e.g. {"bearerAuth": {}}.
Security []map[string][]string `json:"security,omitempty"`
}
OASOperation is an Operation Object.
type OASParameter ¶
type OASParameter struct {
Name string `json:"name"`
In string `json:"in"` // "path" | "query"
Required bool `json:"required,omitempty"`
Description string `json:"description,omitempty"`
Schema *OASSchema `json:"schema,omitempty"`
Explode *bool `json:"explode,omitempty"`
Style string `json:"style,omitempty"`
}
OASParameter is a Parameter Object (path or query).
type OASRequestBody ¶
type OASRequestBody struct {
Description string `json:"description,omitempty"`
Required bool `json:"required"`
Content map[string]OASMediaType `json:"content"`
}
OASRequestBody is a Request Body Object.
func JSONRequestBody ¶
func JSONRequestBody(schema *OASSchema) *OASRequestBody
JSONRequestBody is a convenience constructor that wraps an *OASSchema in a required application/json request body — the common case for action endpoints.
type OASResponse ¶
type OASResponse struct {
Description string `json:"description"`
Content map[string]OASMediaType `json:"content,omitempty"`
}
OASResponse is a Response Object.
type OASSchema ¶
type OASSchema struct {
// Core
Ref string `json:"$ref,omitempty"`
Type any `json:"type,omitempty"` // string | []string
Format string `json:"format,omitempty"`
Description string `json:"description,omitempty"`
// Object
Properties map[string]*OASSchema `json:"properties,omitempty"`
Required []string `json:"required,omitempty"`
AdditionalProperties *bool `json:"additionalProperties,omitempty"`
// Array
Items *OASSchema `json:"items,omitempty"`
// Validation
Enum []any `json:"enum,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
// Annotations
ReadOnly bool `json:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty"`
// Composition
AllOf []*OASSchema `json:"allOf,omitempty"`
AnyOf []*OASSchema `json:"anyOf,omitempty"`
OneOf []*OASSchema `json:"oneOf,omitempty"`
}
OASSchema is a JSON Schema 2020-12 / OAS 3.1 schema object.
The Type field is `any` because OAS 3.1 allows both a single string ("string") and an array of strings (["string", "null"]) for nullable types.
type OASSecurityScheme ¶
type OASSecurityScheme struct {
Type string `json:"type"`
Scheme string `json:"scheme,omitempty"`
BearerFormat string `json:"bearerFormat,omitempty"`
In string `json:"in,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}
OASSecurityScheme is a Security Scheme Object.
type OnDeleteAction ¶
type OnDeleteAction string
OnDeleteAction is a referential action applied to FK columns when the referenced row is deleted.
const ( OnDeleteNoAction OnDeleteAction = "" // default — no constraint clause emitted OnDeleteCascade OnDeleteAction = "cascade" // DELETE parent → DELETE children OnDeleteSetNull OnDeleteAction = "setNull" // DELETE parent → SET fk = NULL OnDeleteRestrict OnDeleteAction = "restrict" // DELETE parent → ERROR if children exist )
type OpenAPIComponents ¶
type OpenAPIComponents struct {
Schemas map[string]*OASSchema `json:"schemas,omitempty"`
SecuritySchemes map[string]OASSecurityScheme `json:"securitySchemes,omitempty"`
}
OpenAPIComponents holds reusable component objects.
type OpenAPIContext ¶
type OpenAPIContext struct {
Request *http.Request
Writer http.ResponseWriter
Ctx context.Context
// Auth is populated by the Auth step (same pattern as ServerContext).
Auth *AuthInfo
// Spec is set by the Generate step. Middleware registered After Generate
// can read and mutate it to add custom paths, extensions, or security schemes.
Spec *OpenAPISpec
// Response overrides the default JSON serialisation when set by middleware.
// If nil after the Response step runs, the default handler writes Spec as JSON.
Response *APIResponse
// contains filtered or unexported fields
}
OpenAPIContext is the shared object threaded through the OpenAPI pipeline. It is separate from ServerContext because the OpenAPI endpoint is not tied to any specific model or CRUD operation.
func (*OpenAPIContext) Abort ¶
func (c *OpenAPIContext) Abort(statusCode int, code, message string)
Abort sets an error response, short-circuiting the pipeline.
func (*OpenAPIContext) Get ¶
func (c *OpenAPIContext) Get(key string) (any, bool)
Get retrieves a value stored by an earlier middleware.
func (*OpenAPIContext) Set ¶
func (c *OpenAPIContext) Set(key string, val any)
Set stores a custom value for downstream middleware.
type OpenAPIInfo ¶
type OpenAPIInfo struct {
Title string `json:"title"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
}
OpenAPIInfo is the Info Object.
type OpenAPIMiddlewareFunc ¶
type OpenAPIMiddlewareFunc func(ctx *OpenAPIContext, next func() error) error
OpenAPIMiddlewareFunc is the middleware signature for the OpenAPI pipeline. Call next() to proceed; return without calling next() to short-circuit.
type OpenAPIPipeline ¶
type OpenAPIPipeline struct {
Auth *OpenAPIStepRegistry
Generate *OpenAPIStepRegistry
Response *OpenAPIStepRegistry
}
OpenAPIPipeline is the middleware injection surface for the spec endpoint.
The three steps run in order:
Auth → guard access (passthrough by default) Generate → build *OpenAPISpec from the registry (default) or replace it Response → serialise Spec to JSON (default)
Usage:
// Require a Bearer token to see the spec
server.Pipeline.OpenAPI.Auth.Register(myAuthMiddleware)
// Inject a custom extension after generation
server.Pipeline.OpenAPI.Generate.Register(func(ctx *maniflex.OpenAPIContext, next func() error) error {
if err := next(); err != nil { return err }
ctx.Spec.Info.Description = "My custom API"
ctx.Spec.Components.SecuritySchemes = map[string]maniflex.OASSecurityScheme{
"bearerAuth": {Type: "http", Scheme: "bearer", BearerFormat: "JWT"},
}
return nil
}, maniflex.After)
// Serve a hand-crafted spec file instead
server.Pipeline.OpenAPI.Generate.Register(serveStaticSpec, maniflex.Replace)
type OpenAPIServer ¶
type OpenAPIServer struct {
URL string `json:"url"`
Description string `json:"description,omitempty"`
}
OpenAPIServer is a Server Object.
type OpenAPISpec ¶
type OpenAPISpec struct {
OpenAPI string `json:"openapi"`
Info OpenAPIInfo `json:"info"`
Servers []OpenAPIServer `json:"servers,omitempty"`
Tags []OpenAPITag `json:"tags,omitempty"`
Paths map[string]PathItem `json:"paths"`
Components OpenAPIComponents `json:"components"`
}
OpenAPISpec is the root OpenAPI 3.1.0 document. It is populated by the Generate pipeline step and serialised to JSON by the Response step. Middleware registered on Pipeline.OpenAPI.Generate can read and mutate it freely.
func GenerateSpec ¶
func GenerateSpec(reg RegistryAccessor, cfg *Config, actions []ActionConfig, search ...*GlobalSearchConfig) *OpenAPISpec
GenerateSpec builds a complete OpenAPI 3.1.0 document from all registered models and custom actions. It is called by the default Generate pipeline step and can be called directly by middleware to get the base spec before customisation.
The optional trailing search argument documents the built-in /search endpoint when the app enabled it via Server.EnableGlobalSearch; it is variadic only so existing direct callers keep compiling.
type OpenAPIStepRegistry ¶
type OpenAPIStepRegistry struct {
// contains filtered or unexported fields
}
OpenAPIStepRegistry holds middlewares for one OpenAPI pipeline step. Unlike StepRegistry it has no ForModel/ForOperation filtering because the OpenAPI endpoint is a single, model-agnostic route.
func (*OpenAPIStepRegistry) Register ¶
func (s *OpenAPIStepRegistry) Register(fn OpenAPIMiddlewareFunc, pos ...Position)
Register adds a middleware to this step.
pos controls where relative to the default handler the middleware runs:
Before (default) — runs before the default handler After — runs after the default handler Replace — replaces the default handler entirely
Examples:
// Require admin role to read the spec pipeline.OpenAPI.Auth.Register(requireAdmin) // Add a custom extension after the spec is generated pipeline.OpenAPI.Generate.Register(addMyExtension, maniflex.After) // Serve the spec from a static file instead of generating it pipeline.OpenAPI.Generate.Register(serveFromFile, maniflex.Replace)
type OpenAPITag ¶
type OpenAPITag struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
OpenAPITag is a Tag Object used for grouping operations in Swagger UI.
type Operation ¶
type Operation string
Operation identifies which CRUD action a request performs.
const ( OpCreate Operation = "create" OpRead Operation = "read" OpUpdate Operation = "update" OpDelete Operation = "delete" OpList Operation = "list" OpOptions Operation = "options" OpHead Operation = "head" // OpReadAttachment identifies a request for GET /:model/:id/:file_field — // the per-model attachment route. Runs Auth → Deserialize → Validate → DB // (FindByID) like a regular Read, then the Response step streams the // referenced file instead of writing a JSON envelope. // // Middleware filtered with ForOperation(OpRead) does NOT match attachment // requests; use ForOperation(OpRead, OpReadAttachment) to apply to both. OpReadAttachment Operation = "read_attachment" // OpAction identifies a request handled by a custom action registered via // server.Action(). Deserialize, Validate, Service, and DB steps are skipped; // Auth and Response run normally. // Note: registering middleware on Pipeline.Deserialize/Validate/Service/DB // with ForOperation(OpAction) is valid syntax but has no effect — those steps // do not execute for actions. The server logs a warning at startup for such a // registration (see warnIneffectiveMiddleware in pipeline.go). OpAction Operation = "action" // OpSearch identifies a request to the built-in cross-model search endpoint // (GET /search, enabled via Server.EnableGlobalSearch). Like OpAction it runs // only Auth → handler → Response; Deserialize, Validate, Service, and DB are // skipped, so the handler performs the search via ctx.Search. // // The synthetic model name is "__search", so ForModel never matches it; gate // middleware on this endpoint with ForOperation(OpSearch) or register it // globally on Pipeline.Auth. Registering on Deserialize/Validate/Service/DB // with ForOperation(OpSearch) has no effect and is warned about at startup // (see warnIneffectiveMiddleware in pipeline.go). OpSearch Operation = "search" // OpExport identifies a request for GET /:model/export — the auto-generated // CSV/XLSX export endpoint. The DB step reads with the same filter/sort as // OpList (no pagination; capped at ModelConfig.MaxExportRows). The Response // step streams CSV (default) or XLSX (?format=xlsx) directly instead of // emitting a JSON envelope. // // Middleware filtered with ForOperation(OpList) does NOT match exports; // use ForOperation(OpList, OpExport) to cover both. OpExport Operation = "export" )
type PathItem ¶
type PathItem struct {
Get *OASOperation `json:"get,omitempty"`
Post *OASOperation `json:"post,omitempty"`
Put *OASOperation `json:"put,omitempty"`
Patch *OASOperation `json:"patch,omitempty"`
Delete *OASOperation `json:"delete,omitempty"`
}
PathItem holds all operations for one URL path.
type Pinger ¶
Pinger is satisfied by any DB adapter that exposes a Ping method. It is deliberately narrow — only the health handler uses it, so we avoid adding Ping to the full DBAdapter interface (which would break all custom adapters written against the previous interface).
*sqlcore.Adapter satisfies Pinger automatically because it wraps *sql.DB, which has PingContext. Custom adapters that do not embed *sql.DB can add:
func (a *MyAdapter) Ping(ctx context.Context) error { return a.db.PingContext(ctx) }
type Pipeline ¶
type Pipeline struct {
// Auth — identity and access control (passthrough by default).
Auth *StepRegistry
// Deserialize — parse JSON body into ParsedBody; parse URL query params into Query.
Deserialize *StepRegistry
// Validate — enforce mfx struct-tag rules (required, readonly, immutable, enum, min/max).
Validate *StepRegistry
// Service — user-injectable business logic (noop by default).
Service *StepRegistry
// DB — dispatch to the configured DBAdapter.
DB *StepRegistry
// Response — build the JSON response envelope from ctx.DBResult.
Response *StepRegistry
// OpenAPI — three-step pipeline for the GET /openapi.json endpoint.
// Steps: Auth → Generate → Response
OpenAPI *OpenAPIPipeline
}
Pipeline is the public middleware injection surface. It holds one StepRegistry per pipeline step, in execution order:
Auth → Deserialize → Validate → Service → DB → Response
It also holds an OpenAPIPipeline for the schema endpoint:
OpenAPI.Auth → OpenAPI.Generate → OpenAPI.Response
Inject middleware into any step via its Register() method:
server.Pipeline.Auth.Register(myJWTMiddleware)
server.Pipeline.Service.Register(myBusinessLogic, maniflex.ForModel("Order"))
server.Pipeline.DB.Register(myAuditLogger, maniflex.AtPosition(maniflex.After))
server.Pipeline.OpenAPI.Auth.Register(requireAdmin)
server.Pipeline.OpenAPI.Generate.Register(addExtensions, maniflex.After)
type PipelineTrace ¶
type PipelineTrace struct {
// Enabled is a shorthand that activates Steps, Timings, and Aborts when set
// to true without any sub-flags being explicitly configured.
// Bodies and Skips are NOT turned on by Enabled — they are opt-in because
// they are high-volume or may expose sensitive request data.
Enabled bool
// Steps logs an "enter" record before each named middleware runs and an
// "exit" record after it returns, with step name and middleware name.
Steps bool
// Timings adds an elapsed duration to each "exit" record. Requires Steps.
Timings bool
// Aborts logs when ctx.Abort() is called: the HTTP status, error code, and
// the source file:line of the Abort call site inside the middleware.
Aborts bool
// Bodies logs the field names present in ctx.ParsedBody after the Deserialize
// step. WARNING: may expose sensitive fields (passwords, tokens). Disabled by
// Enabled; must be set explicitly.
Bodies bool
// Skips logs when a registered middleware is skipped because its ForModel or
// ForOperation filter did not match the current request.
Skips bool
}
PipelineTrace controls per-request debug tracing through the middleware pipeline. All output is at DEBUG level; set Config.Logger to a handler that accepts DEBUG records to see it.
type Position ¶
type Position int
Position controls where in a step's execution chain a middleware is inserted.
type QueryParams ¶
type QueryParams struct {
Page int
Limit int
Filters []*FilterExpr
Sorts []SortExpr
Includes []string // relation keys to load inline
Fields []string // DB column names to SELECT; empty = SELECT table.*
// Search holds the trimmed ?q= full-text search query, "" when absent. When
// non-empty the DB step adds the driver's native FTS predicate over the
// model's mfx:"searchable" columns and orders results by relevance. Only set
// on models that declare searchable fields; ParseQueryParams rejects ?q= on
// any other model with a 400.
Search string
// Cursor, when non-nil, switches the list query to keyset (cursor)
// pagination: the DB step walks the dataset ordered by (cursor field, id)
// with a WHERE bound instead of LIMIT/OFFSET, and skips the COUNT. Set by
// ParseQueryParams when ?cursor= is present on a cursor-enabled model.
Cursor *CursorParams
}
QueryParams holds all URL query parameters parsed for a list request.
func ParseQueryParams ¶
func ParseQueryParams(r *http.Request, model *ModelMeta, reg RegistryAccessor) (*QueryParams, error)
ParseQueryParams parses page, limit, filter, sort, and include parameters.
?page=2&limit=25 ?filter=status:eq:active&filter=author.role:neq:banned ?sort=created_at:desc,title:asc ?include=author,category
func (*QueryParams) HasInclude ¶
func (q *QueryParams) HasInclude(key string) bool
HasInclude reports whether key is in the include list.
func (*QueryParams) Offset ¶
func (q *QueryParams) Offset() int
Offset returns the DB offset for the current page.
type RawResult ¶
type RawResult interface {
// When `Raw` called without select (as Exec), otherwise returns (nil, nil)
LastInsertId() (*int64, error)
// When `Raw` called without select (as Exec), otherwise returns (nil, nil)
RowsAffected() (*int64, error)
// Result of query if `Raw` called with select, otherwise returns (nil, nil)
Rows() (*sql.Rows, error)
}
type RecursiveDirection ¶
type RecursiveDirection string
RecursiveDirection controls which direction a recursive CTE traversal walks.
const ( // RecursiveDescendants walks the tree downward, collecting all children of // the root node. This is the default when Direction is unset. RecursiveDescendants RecursiveDirection = "descendants" // RecursiveAncestors walks the tree upward, collecting all parents of the // starting node up to the root. RecursiveAncestors RecursiveDirection = "ancestors" )
type RecursiveQuery ¶
type RecursiveQuery struct {
// RootID is the id of the starting node. Required.
RootID string
// ParentField is the DB column that holds the parent's id.
// e.g. "parent_id", "manager_id". Required; must be a registered column.
ParentField string
// MaxDepth limits how many levels to traverse. 0 means unlimited.
// MaxDepth=1 returns the root plus its immediate children/parent only.
MaxDepth int
// Direction controls descent (Descendants, default) or ascent (Ancestors).
Direction RecursiveDirection
// Where are additional filters applied in both the anchor and recursive
// members. Nested-relation filters are not supported and return an error.
Where []*FilterExpr
}
RecursiveQuery is the input to ctx.RecursiveQuery.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is a thread-safe, ordered store of registered ModelMeta values.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry creates a new empty Registry. Exported for use in tests.
func (*Registry) AddForTest ¶
AddForTest adds a ModelMeta to the registry. Exported for use in tests.
type RegistryAccessor ¶
RegistryAccessor is the read-only interface DB adapters receive.
type RelationKind ¶
type RelationKind int
RelationKind describes the direction of a relationship.
const ( // BelongsTo: this model holds the FK. e.g. Post.UserID → User BelongsTo RelationKind = iota // HasMany: the related model holds the FK. e.g. User → []Post HasMany // ManyToMany: a junction table connects two models. e.g. Product ↔ Tag via ProductTag. ManyToMany )
type RelationMeta ¶
type RelationMeta struct {
// FieldName is the Go FK field name for BelongsTo (e.g. "ManagerID")
// or the slice field name for HasMany (e.g. "Posts").
FieldName string
// DBName is the snake_case version of FieldName.
DBName string
// FKColumn is the DB column carrying the foreign key.
// BelongsTo → column on THIS table (e.g. "manager_id")
// HasMany → column on the RELATED table (e.g. "team_id")
FKColumn string
// RelationKey is the short key used in ?include= and nested ?filter=.
// Explicit: snake_case of the Relation tag value, e.g. "manager"
// Convention: snake_case of trimmed field name, e.g. "user"
RelationKey string
// CompanionField is the Go struct field name of the companion placeholder
// (e.g. "Manager" for a ManagerID FK). Empty for convention-style or HasMany.
CompanionField string
// RelatedModel is the target model's struct name, e.g. "User".
RelatedModel string
// Convention is true when this BelongsTo was inferred from a "<Name>ID" field
// name rather than an explicit mfx:"relation:X" tag. Used to warn (not error)
// when such an inferred relation targets a model that was never registered —
// the common microservice case of storing a foreign id (e.g. UserID) by
// design, which mfx:"norelation" silences.
Convention bool
Kind RelationKind
OnDelete OnDeleteAction
// ManyToMany-only fields. All three are populated by resolveManyToMany.
ThroughTable string // junction table, e.g. "product_tags"
ThroughLocalFK string // FK on junction pointing to THIS model, e.g. "product_id"
ThroughRemoteFK string // FK on junction pointing to the related model, e.g. "tag_id"
ThroughModel string // junction model name, e.g. "ProductTag"
}
RelationMeta describes one relationship on a model.
type RequestBody ¶
type RequestBody struct {
// contains filtered or unexported fields
}
RequestBody holds the parsed, JSON-keyed request body. It is read-only from outside the maniflex package: the only way to mutate it is ctx.SetField / ctx.DeleteField, which also keep the typed record (ctx.Record) in sync.
This is deliberate. When ParsedBody was a bare map[string]any, middleware could write ctx.ParsedBody["k"] = v directly — bypassing the record sync — and the typed write path would then silently drop the change (it sources columns from the in-sync record when the present-key sets match). Wrapping the map makes that bypass a compile error: there is no exported way to set a key except through ctx.SetField.
All methods are nil-safe, so ctx.ParsedBody can be compared to nil (no body on the request) and still be read without a guard.
func NewRequestBody ¶
func NewRequestBody(m map[string]any) *RequestBody
NewRequestBody wraps a JSON-keyed map as a RequestBody. The pipeline builds one during Deserialize; this constructor is for callers (mainly tests) that build a ServerContext directly.
func (*RequestBody) Get ¶
func (b *RequestBody) Get(key string) (any, bool)
Get returns the value for a JSON key and whether the key was present (an explicit null is present with a nil value).
func (*RequestBody) Has ¶
func (b *RequestBody) Has(key string) bool
Has reports whether a JSON key is present, regardless of its value.
func (*RequestBody) Keys ¶
func (b *RequestBody) Keys() []string
Keys returns the top-level JSON keys in unspecified order.
func (*RequestBody) Len ¶
func (b *RequestBody) Len() int
Len returns the number of top-level keys in the body.
func (*RequestBody) Map ¶
func (b *RequestBody) Map() map[string]any
Map returns a shallow copy of the body as a plain map, for read-only consumers such as validation callbacks and ABAC policies. Mutating the returned map does NOT change the body — use ctx.SetField / ctx.DeleteField for that.
type ResponseMeta ¶
type ResponseMeta struct {
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
Pages int64 `json:"pages"`
Dir string `json:"_dir,omitempty"` // "rtl" when the active locale uses right-to-left script
// Cursor-mode fields. Cursor is set to render the keyset shape; NextCursor is
// the token for the following page ("" on the last page).
Cursor bool `json:"-"`
NextCursor string `json:"-"`
HasMore bool `json:"-"`
}
ResponseMeta carries pagination metadata for list responses. It serialises in one of two shapes depending on the pagination mode (see MarshalJSON): offset mode emits {total, page, limit, pages}; cursor (keyset) mode emits {limit, next_cursor, has_more}.
func (ResponseMeta) MarshalJSON ¶
func (m ResponseMeta) MarshalJSON() ([]byte, error)
MarshalJSON renders the offset shape ({total, page, limit, pages}) by default, and the cursor shape ({limit, next_cursor, has_more}) when Cursor is set, so a keyset response never carries meaningless total/page/pages fields.
type Row ¶
Row is the sanctioned shape for schemaless query results — raw SQL, aggregates, and recursive queries, whose columns aren't a registered model. It is an alias for map[string]any, so existing code keeps compiling; the named type documents intent and gives the dynamic escape hatch a single home as the typed-models migration removes ad-hoc maps elsewhere.
type SQLTyper ¶
type SQLTyper interface {
SQLType(driver DriverType) string
}
SQLTyper is implemented by Go types that need a driver-specific SQL column type instead of the default mapping in goTypeToSQL. The adapter calls SQLType before its built-in type switch, so any type implementing this interface controls its own schema column definition.
type Amount struct { ... }
func (Amount) SQLType(driver maniflex.DriverType) string {
if driver == maniflex.Postgres { return "NUMERIC(19,4)" }
return "TEXT"
}
type SchedAction ¶
type SchedAction uint8
SchedAction is the action a scheduled time-driven transition applies to a row once its timestamp column falls in the past (8.6).
const ( // SchedSoftDelete soft-deletes the row (model must be soft-deletable). SchedSoftDelete SchedAction = iota // SchedHardDelete physically deletes the row. SchedHardDelete // SchedSetField writes a fixed value into a sibling column. SchedSetField )
type ScheduledSpec ¶
type ScheduledSpec struct {
Column string // DB column of the *time.Time field that drives it
Action SchedAction // resolved action
Field string // target DB column (SchedSetField only)
From string // guard value (SchedSetField only)
HasFrom bool // whether From was specified (SchedSetField only)
To string // value to write (SchedSetField only)
}
ScheduledSpec is one resolved scheduled field of a model (8.6).
type SearchOptions ¶
type SearchOptions struct {
// Query is the free-form search text (the ?q= value). Blank (after trimming)
// is a no-op: ctx.Search returns (nil, nil).
Query string
// Models names the registered models to search. Each must be registered and
// declare at least one mfx:"searchable" field, else ctx.Search errors. When
// empty, ctx.Search searches every model whose ModelConfig.GlobalSearchable
// is set (the model set the built-in /search endpoint uses).
Models []string
// Limit caps the number of merged results returned. <= 0 defaults to 20.
Limit int
// PerModelLimit is a fairness "fair chance" cap, NOT a hard ceiling. When > 0,
// the merge first takes at most this many of each model's top-scoring hits,
// then backfills any slots still free up to Limit from the remaining
// candidates by score — regardless of model — so a single model MAY exceed the
// cap during backfill rather than leave the result short. <= 0 merges purely
// by score.
PerModelLimit int
}
SearchOptions configures a cross-model full-text search run by ctx.Search.
type SearchResult ¶
type SearchResult struct {
Model string `json:"model"` // registered model name the hit came from
ID string `json:"id"` // primary key of the matched row
Snippet string `json:"snippet"` // highlighted excerpt of the matched text
Score float64 `json:"score"` // relevance, higher = more relevant
}
SearchResult is one hit in the merged cross-model search result.
type Server ¶
type Server struct {
Pipeline *Pipeline
// contains filtered or unexported fields
}
Server is the top-level server.
Typical usage — signals handled automatically:
server := maniflex.New(maniflex.Config{DB: myAdapter})
server.MustRegister(User{}, Post{}, Comment{})
server.Pipeline.Auth.Register(jwtMiddleware)
if err := server.Start(); err != nil {
log.Fatal(err)
}
Advanced usage — caller controls the shutdown context:
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := server.StartWithContext(ctx); err != nil {
log.Fatal(err)
}
func New ¶
New creates a Server with the given configuration. Sensible defaults are applied for any zero-value fields.
func (*Server) Action ¶
func (c *Server) Action(cfg ActionConfig)
Action registers a custom HTTP endpoint that participates in the Auth and Response pipeline steps. Deserialize, Validate, Service, and DB steps are skipped; the handler is responsible for body parsing (ctx.BindJSON) and setting ctx.Response or calling ctx.Abort.
Must be called before Start() or Handler(). Panics if the server has already started or if the method+path conflicts with a registered model route.
func (*Server) AddComputedField ¶
func (s *Server) AddComputedField(modelName, name string, fn ComputedFunc) error
AddComputedField registers a derived field on the named model. The field appears in every read response (single read, create/update echo, list rows) for that model. Returns an error when the model is not registered or the field name collides with an existing real field or another computed field.
server.AddComputedField("Product", "stock_level",
func(ctx context.Context, row map[string]any) (any, error) {
return stockService.CurrentLevel(ctx, row["id"].(string))
})
func (*Server) AddService ¶
AddService registers a long-lived background Service supervised by the server. Services start after migration and DB-ready, in registration order, before the HTTP listener opens, and stop in reverse order during graceful shutdown (before the background-goroutine drain). A Start error aborts boot.
Must be called before Start/StartWithContext.
server.AddService(pool) // a custom Service server.AddService(maniflex.ServiceFunc(startFn)) // adapter for a bare func
func (*Server) DB ¶
DB returns the configured database adapter. It is used by packages such as jobs/maniflex that need to write directly to the database outside the HTTP pipeline (e.g. StatusSink.Transition called from a background worker).
func (*Server) EnableGlobalSearch ¶
func (c *Server) EnableGlobalSearch(cfg ...GlobalSearchConfig)
EnableGlobalSearch mounts the built-in cross-model search endpoint at {PathPrefix}{Path} (default GET /search). It fans the native full-text search out over every model with ModelConfig.GlobalSearchable set and merges the hits into one relevance-ranked {"data": [{model, id, snippet, score}, ...]} list.
The endpoint runs only the global Auth pipeline step (and Response) — it does NOT apply per-model auth/tenancy middleware — so only opt models into GlobalSearchable that are safe to search this way, and gate the endpoint with Pipeline.Auth middleware (globally or ForOperation(OpSearch)). For a scoped search with the app's own authorisation, build a custom Action that calls ctx.Search with an explicit model list instead.
Must be called before Start() or Handler(). Apps that never call it gain no new endpoint.
func (*Server) Go ¶
Go runs fn on an application-scoped goroutine that the server drains during graceful shutdown — the same lifecycle as ctx.GoBackground, but tied to the server rather than a single request. The ctx passed to fn is cancelled when shutdown begins, so a long-running loop (e.g. a periodic reconciler) returns on ctx.Done() and is then waited on, bounded by Config.ShutdownTimeout.
server.Go(func(ctx context.Context) {
t := time.NewTicker(time.Minute)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
reconcile(ctx)
}
}
})
func (*Server) Handler ¶
Handler returns the underlying http.Handler without starting the server. Useful for testing or embedding into an existing HTTP mux.
Unlike Start, Handler does NOT run auto-migration — call Start, MigrateOnly, or the adapter's AutoMigrate first, or requests will fail against missing tables.
func (*Server) MigrateOnly ¶
MigrateOnly runs auto-migration (if enabled in Config) and returns without starting the HTTP server. Use this for the Kubernetes init-container pattern where migrations run as a separate, single-shot process before the main server replicas start serving traffic:
if os.Getenv("MIGRATE_ONLY") == "1" {
log.Fatal(server.MigrateOnly(ctx))
}
log.Fatal(server.Start())
func (*Server) MustAddComputedField ¶
func (s *Server) MustAddComputedField(modelName, name string, fn ComputedFunc)
MustAddComputedField is the panic-on-error variant, intended for use in `main` or package initialisation.
func (*Server) MustRegister ¶
MustRegister calls Register and panics on error. Intended for use in package-level init or main().
func (*Server) PathPrefix ¶
PathPrefix returns the configured route prefix (e.g. "/api"). Satellite packages such as maniflex/admin use it to address the generated API in-process.
func (*Server) RealtimeDoc ¶
func (c *Server) RealtimeDoc(cfg AsyncAPIConfig)
RealtimeDoc enables the {PathPrefix}/asyncapi.json endpoint, which serves an AsyncAPI 2.6 document describing the realtime event channels clients can subscribe to over the realtime hub (see the realtime package). Declare custom event payloads via cfg.Events and/or set cfg.AutoModelEvents to derive <model>.created|updated|deleted channels from the registry.
Must be called before Start() or Handler(). Apps that never call it gain no new endpoint.
func (*Server) Register ¶
Register adds one or more models to the Server. It accepts any of:
server.Register(User{})
server.Register(User{}, ModelConfig{TableName: "members"})
server.Register(User{}, Post{}, Comment{})
server.Register([]any{User{}, Post{}})
func (*Server) Registry ¶
func (c *Server) Registry() RegistryAccessor
Registry returns the read-only model registry.
func (*Server) SetDB ¶
SetDB injects or replaces the database adapter after construction. This allows the two-step init pattern:
server := maniflex.New(maniflex.Config{...})
server.MustRegister(User{}, Post{})
db, _ := sqlite.Open(":memory:", server.Registry())
server.SetDB(db)
server.Start()
func (*Server) SetKeyProvider ¶
func (c *Server) SetKeyProvider(kp KeyProvider)
SetKeyProvider injects or replaces the KeyProvider after construction. This allows the two-step init pattern:
server := maniflex.New(maniflex.Config{...})
server.SetKeyProvider(&encryption.EnvKeyProvider{Prefix: "MYAPP_KEY"})
func (*Server) SetStorage ¶
func (c *Server) SetStorage(fs FileStorage)
SetStorage injects or replaces the file storage backend after construction. This allows the two-step init pattern (analogous to SetDB):
server := maniflex.New(maniflex.Config{...})
fs, _ := storage.NewLocalStorage("./uploads")
server.SetStorage(fs)
func (*Server) Shutdown ¶
Shutdown initiates a graceful shutdown of the running server and waits for in-flight requests AND tracked background goroutines (audit writes, cache invalidations) to complete, bounded by the provided context. If the server is not running, Shutdown is a no-op.
Typical usage in tests or when embedding the server:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server.Shutdown(ctx)
func (*Server) Start ¶
Start performs auto-migration (if enabled), starts the HTTP server, and listens for SIGINT or SIGTERM. When a signal arrives it initiates a graceful shutdown: in-flight requests are given up to Config.ShutdownTimeout to complete before the server closes. Start returns nil if shutdown completes cleanly, or an error if the shutdown context expires or startup fails.
Start is the correct entry point for production. It is equivalent to:
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() server.StartWithContext(ctx)
func (*Server) StartWithContext ¶
StartWithContext is like Start but uses the provided context to drive shutdown instead of OS signals. The server runs until ctx is cancelled, then performs a graceful shutdown with a fresh timeout context derived from Config.ShutdownTimeout.
This variant is useful when:
- The caller manages its own signal handling.
- The server must shut down in response to application-level events.
- Tests need to stop the server without sending a real OS signal.
Example:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() server.StartWithContext(ctx) // server stops after 5 minutes
type ServerContext ¶
type ServerContext struct {
// HTTP primitives
Request *http.Request
Writer http.ResponseWriter
Ctx context.Context
// Routing context — set by the handler before pipeline.execute()
Model *ModelMeta
Operation Operation
ResourceID string // primary key from the URL, present for read/update/delete
// AttachmentField is set when Operation == OpReadAttachment. It points at
// the file field whose contents the request is asking to stream. The
// Response step consults it to dereference the storage key from
// DBResult and serve the file. nil for all other operations.
AttachmentField *FieldMeta
// RequestID is the value of the X-Request-Id header set by chi's RequestID
// middleware. It is extracted once in handler.dispatch and stored here so
// every middleware can include it in structured log records without importing
// chi or reading the HTTP header directly.
//
// The same value is echoed back in the X-Request-Id response header so
// clients can correlate requests to log lines.
RequestID string
// TraceID is the value of the W3C traceparent header on the incoming
// request, or empty when the header is absent or malformed. The full
// header value is preserved verbatim so it can be forwarded to downstream
// services (e.g. service.Webhook) without re-encoding.
TraceID string
// Step outputs — each step populates these in order
RawBody []byte // raw request body bytes (set by Deserialize)
// ParsedBody is the deserialized JSON body, JSON-keyed (set by Deserialize).
// It is read-only: read via ctx.Field or ParsedBody's methods (Get/Has/Len/
// Keys/Map), and mutate ONLY via ctx.SetField / ctx.DeleteField, which keep
// the typed Record in sync. It is a *RequestBody (not a bare map) precisely so
// a raw ctx.ParsedBody["k"]=v — which bypassed that sync and could drop the
// write — is a compile error. nil when the request carries no body.
//
// Prefer Record (the typed *T carrier) where possible.
ParsedBody *RequestBody
// Record holds the typed record carrier (*T for ctx.Model.GoType) for write
// operations (set by Deserialize) and, increasingly, read results. The
// pipeline reads/writes record fields through it instead of ParsedBody.
Record any
Query *QueryParams // parsed URL query params (set by Deserialize)
DBResult any // *T, *ListResult, or map[string]any (set by DB step)
Response *APIResponse // final response envelope (set by Response step)
// Files holds parsed file uploads from multipart/form-data requests.
// Keyed by the form field name (which matches the JSON field name).
// Populated by the Deserialize step when Content-Type is multipart/form-data.
Files map[string]*UploadedFile
// Locale is the explicit locale requested by the client, resolved by
// LocaleResolver middleware from ?locale= or Accept-Language. Empty when
// the client did not request a specific locale. Used as the first entry in
// the locale resolution chain for LocaleString fields.
Locale string
// DefaultLocale is the app-configured fallback locale from LocaleOptions.Default.
// Set by LocaleResolver; empty when the middleware is not registered (falls
// back to "en" in the resolution chain).
DefaultLocale string
// SplitSuffix is the suffix used for the i18n companion field in split mode
// (e.g. "name_i18n"). Set by LocaleResolver from LocaleOptions.SplitSuffix;
// defaults to "_i18n" when the middleware is not registered.
SplitSuffix string
// DefaultLocaleMode is the app-wide fallback LocaleMode from
// LocaleOptions.DefaultLocaleMode. Set by LocaleResolver; empty when the
// middleware is not registered (falls back to LocaleModeSplit).
DefaultLocaleMode LocaleMode
// Auth — set by the Auth step (your middleware fills this)
Auth *AuthInfo
// Tx is an optional active transaction. When non-nil the default DB step
// routes all Create / Update / Delete / FindByID / FindMany calls through
// it instead of the bare adapter, so every DB operation in the request
// participates in the same transaction.
//
// Set this field from a Service or DB middleware using BeginTx:
//
// tx, err := ctx.BeginTx(ctx.Ctx, nil)
// if err != nil { ... }
// ctx.Tx = tx
// defer tx.Rollback() // no-op after Commit
Tx Tx
// contains filtered or unexported fields
}
ServerContext is the single shared object threaded through every pipeline step for one HTTP request. Steps read from it, write to it, and call next() to proceed to the following step.
func NewBackground ¶
func NewBackground(ctx context.Context, adapter DBAdapter, reg RegistryAccessor) *ServerContext
NewBackground constructs a minimal ServerContext for use outside the HTTP pipeline — background workers, tests, and CLI commands. The returned context has no HTTP request wired up; operations that require ctx.Request will panic.
Example (tests):
bgCtx := maniflex.NewBackground(context.Background(), srv.DB(), srv.Registry()) entry, err := l.Post(bgCtx, ...)
func (*ServerContext) Abort ¶
func (c *ServerContext) Abort(statusCode int, code, message string)
Abort populates ctx.Response with an error and should be used by middleware to short-circuit the pipeline. The current step must then return nil without calling next() for the abort to take effect.
func (*ServerContext) Aggregate ¶
func (c *ServerContext) Aggregate(modelName string, agg AggregateQuery) ([]Row, error)
Aggregate runs a structured aggregation query against the named model. It validates field names against the model's registered DB columns so an invalid input fails fast with a clear error rather than producing a SQL syntax error from the driver.
The query participates in ctx.Tx when one is active; otherwise it uses the adapter's read pool. The returned rows are one map per group; aggregate values are accessed by alias.
rows, err := ctx.Aggregate("Order", maniflex.AggregateQuery{
Select: []maniflex.AggregateField{
{Op: maniflex.AggCount, As: "n"},
{Op: maniflex.AggSum, Field: "total", As: "revenue"},
},
GroupBy: []string{"status"},
Where: []*maniflex.FilterExpr{{Field: "created_at", Operator: maniflex.OpGte, Value: "2026-01-01"}},
OrderBy: []maniflex.SortExpr{{DBName: "revenue", Direction: maniflex.SortDesc}},
Limit: 100,
})
func (*ServerContext) BindJSON ¶
func (c *ServerContext) BindJSON(v any) error
BindJSON decodes the request body as JSON into v. It enforces the same 4 MB body size limit as the Deserialize step and sets ctx.RawBody. An absent body is rejected with 400 EMPTY_BODY — use TryBindJSON for GET or optional-body actions. On error it calls ctx.Abort and returns a non-nil error so the caller can return nil immediately:
var req MyRequest
if err := ctx.BindJSON(&req); err != nil {
return nil // ctx.Abort already called
}
func (*ServerContext) DeleteField ¶
func (c *ServerContext) DeleteField(jsonName string)
DeleteField removes a request-body field by its JSON name from ParsedBody and clears it from the typed record's present set, so the write path skips it. Use it from middleware that strip a field before the DB step.
func (*ServerContext) DriverType ¶
func (c *ServerContext) DriverType() DriverType
DriverType returns the SQL dialect used by the adapter that will serve this request. Useful for packages that build raw SQL and need to choose between ? (SQLite) and $N (Postgres) placeholders.
If the underlying adapter does not implement the optional DriverTyper interface, SQLite is returned as a safe default.
func (*ServerContext) Field ¶
func (c *ServerContext) Field(jsonName string) (any, bool)
Field reads a request-body field by its JSON name. ParsedBody is the authoritative body during the transition, so it is read from there.
func (*ServerContext) Get ¶
func (c *ServerContext) Get(key string) (any, bool)
Get retrieves a value stored by a previous pipeline step or middleware.
func (*ServerContext) GetModel ¶
func (c *ServerContext) GetModel(modelName string) *ModelAccessor
GetModel returns a ModelAccessor for the named registered model. All five CRUD operations on the accessor route through ctx.Tx when one is active. Returns an error accessor that surfaces the lookup failure on first use when the model name is not registered or the registry is unavailable.
func (*ServerContext) GoBackground ¶
func (c *ServerContext) GoBackground(fn func(context.Context))
GoBackground schedules fn on a goroutine tracked by the Server. Server.Shutdown waits for tracked goroutines to complete (bounded by Config.ShutdownTimeout) so audit writes, cache invalidations, and other fire-and-forget work are not truncated by a process exit. The ctx passed to fn is independent of the HTTP request context (which is already returning when GoBackground is called) but IS cancelled when Shutdown's deadline elapses, so well-behaved writers honour that signal and exit promptly.
When ctx.bg is nil (a synthesised ServerContext without server wiring), fn runs on a plain goroutine with context.Background() — no shutdown coupling.
func (*ServerContext) HasRole ¶
func (c *ServerContext) HasRole(role string) bool
HasRole reports whether the authenticated principal holds the given role.
func (*ServerContext) LockForUpdate ¶
func (c *ServerContext) LockForUpdate(modelName, id string) (map[string]any, error)
LockForUpdate acquires a pessimistic row-level lock on the named model's record identified by id and returns the current row data.
For Postgres the underlying query appends FOR UPDATE; the lock is held until the enclosing transaction commits or rolls back. For SQLite the lock is implicit in the transaction isolation level (use BEGIN IMMEDIATE via TxOptions or the _txlock=immediate DSN option).
LockForUpdate requires an active transaction on ctx.Tx. Call ctx.BeginTx or register maniflex.WithTransaction before invoking it.
server.Pipeline.Service.Register(func(ctx *maniflex.ServerContext, next func() error) error {
stockID, _ := ctx.Field("stock_id")
stock, err := ctx.LockForUpdate("StockBalance", stockID.(string))
if err != nil { return err }
if stock["quantity"].(int64) < 1 {
ctx.Abort(409, "OUT_OF_STOCK", "insufficient stock")
return nil
}
return next()
}, maniflex.ForModel("Dispense"), maniflex.ForOperation(maniflex.OpCreate))
func (*ServerContext) Logger ¶
func (c *ServerContext) Logger() *slog.Logger
Logger returns a *slog.Logger pre-seeded with the request_id attribute so every log line emitted from middleware is automatically correlated to the originating HTTP request without any extra boilerplate.
ctx.Logger().Info("record processed", slog.String("id", record["id"].(string)))
The returned logger is memoised across calls within the same request — the underlying attributes (service / request_id / trace_id) don't change once the request enters the pipeline, so per-step middleware can call Logger() in a hot path without paying a fresh base.With(...) on every emission.
func (*ServerContext) QueryModel
deprecated
func (c *ServerContext) QueryModel(modelName string, q *QueryParams) ([]map[string]any, error)
QueryModel reads records from any registered model using the standard FindMany path. When ctx.Tx is active the query participates in it. q may be nil — defaults to page 1, limit 20 with no filters or sorts.
Deprecated: prefer ctx.GetModel(name).List(q) which also exposes Read, Create, Update, and Delete on the same accessor (3D.5).
func (*ServerContext) QueryParam ¶
func (c *ServerContext) QueryParam(name string) string
QueryParam returns the first value of a URL query parameter, or "" if absent. Convenience wrapper around ctx.Request.URL.Query().Get(name).
date := ctx.QueryParam("date") // ?date=2026-05-04
func (*ServerContext) RawExec ¶
func (c *ServerContext) RawExec(query string, args ...any) (int64, error)
RawExec executes a parameterised non-SELECT statement and returns the number of rows affected. When ctx.Tx is active the statement participates in it; otherwise it uses the adapter's write pool.
func (*ServerContext) RawQuery ¶
func (c *ServerContext) RawQuery(query string, args ...any) ([]Row, error)
RawQuery executes a parameterised SELECT query and returns each row as a column-name → value map. When ctx.Tx is active the query participates in the transaction; otherwise it uses the adapter's read pool.
Use driver-appropriate placeholders ($1/$2 for Postgres, ? for SQLite). Never interpolate values directly into the query string.
func (*ServerContext) RecursiveQuery ¶
func (c *ServerContext) RecursiveQuery(modelName string, q RecursiveQuery) ([]Row, error)
RecursiveQuery executes a WITH RECURSIVE CTE against a self-referential model and returns all nodes in the subtree (or ancestor chain) starting at q.RootID. Every returned row contains an integer _depth column (0 = root, 1 = immediate children/parent, …). Rows are ordered by _depth ascending.
Both Postgres (WITH RECURSIVE … $N) and SQLite (WITH RECURSIVE … ?, supported since 3.8.3) are handled transparently.
The query participates in ctx.Tx when one is active; otherwise it uses the adapter's read pool.
rows, err := ctx.RecursiveQuery("Category", maniflex.RecursiveQuery{
RootID: "some-uuid",
ParentField: "parent_id",
MaxDepth: 5,
})
// rows[0]["_depth"] == int64(0) is the root; rows[1..n] are descendants.
func (*ServerContext) Search ¶
func (c *ServerContext) Search(opts SearchOptions) ([]SearchResult, error)
Search runs a cross-model full-text search and returns the merged, relevance- ranked hits. It participates in ctx.Tx when one is active (via ctx.RawQuery); otherwise it uses the adapter's read pool.
hits, err := ctx.Search(maniflex.SearchOptions{
Query: "wireless headphones",
Models: []string{"Post", "Comment"}, // explicit, app-authorised set
Limit: 20,
})
With Models empty it searches every ModelConfig.GlobalSearchable model — the behaviour the built-in GET /search endpoint relies on.
func (*ServerContext) ServiceName ¶
func (c *ServerContext) ServiceName() string
ServiceName returns the Config.ServiceName configured on the Server instance, or "" when none was set. Middleware uses this to enrich audit records, outgoing requests, and observability payloads without holding a reference to the framework Config.
func (*ServerContext) Set ¶
func (c *ServerContext) Set(key string, val any)
Set stores a value that later pipeline steps or middleware can retrieve.
func (*ServerContext) SetField ¶
func (c *ServerContext) SetField(jsonName string, value any)
SetField sets a request-body field by its JSON name, writing through to ctx.ParsedBody and (when a typed record is bound) the matching struct field on ctx.Record, marking it present. Use it from create/update middleware.
func (*ServerContext) TrackStoredFile ¶
func (c *ServerContext) TrackStoredFile(key string, storage FileStorage)
TrackStoredFile records an object written to FileStorage during this request so the framework can delete it (roll back the write) if the request ultimately fails. The default file-upload step calls this after every successful Store; custom Service middleware that writes to storage directly should call it too, passing the backend it used, to get the same orphan-cleanup guarantee (3B.2b).
A request that ends 2xx keeps every tracked file. A pipeline error or a non-2xx final response deletes them all (see cleanupOrphanedFiles).
func (*ServerContext) TryBindJSON ¶
func (c *ServerContext) TryBindJSON(v any) (ok bool, err error)
TryBindJSON behaves like BindJSON but treats an absent body as a non-error, so GET or optional-body actions can share the same size-limited, abort-on- failure parsing path. It returns (false, nil) when the body is empty (v left untouched, ctx not aborted), (false, err) after calling ctx.Abort on an I/O, size, or parse failure, and (true, nil) on success with v populated and ctx.RawBody set:
ok, err := ctx.TryBindJSON(&req)
if err != nil {
return nil // ctx.Abort already called
}
if ok {
// req populated from a present body
}
func (*ServerContext) URLParam ¶
func (c *ServerContext) URLParam(name string) string
URLParam returns the value of a named URL parameter from the current request. Uses chi's URLParam under the hood; action handlers use this instead of importing chi directly.
// Route: /patients/{patientId}/notes/{noteId}
patientID := ctx.URLParam("patientId")
noteID := ctx.URLParam("noteId")
type Service ¶
type Service interface {
// Start is called once, after migration and DB-ready, before the HTTP
// listener opens. It must return promptly — spawn any long-running loop on
// its own goroutine (or via Server.Go) rather than blocking here. The ctx
// is cancelled when shutdown begins, so a loop that selects on ctx.Done()
// winds itself down without waiting for Stop.
//
// A non-nil error aborts boot exactly like a failed migration; services that
// already started are stopped in reverse order first.
Start(ctx context.Context) error
// Stop is called once during graceful shutdown, after the HTTP listener has
// drained and after the Start ctx has been cancelled. The ctx is a fresh
// deadline context bounded by Config.ShutdownTimeout — use it to flush
// buffers, close connections, or wait for in-flight work to settle.
Stop(ctx context.Context) error
}
Service is an application-owned, long-lived background component supervised by the Server: a poller, cache warmer, queue consumer, or an in-memory pool manager. Register one with Server.AddService and the framework wires it into the boot and shutdown lifecycle instead of the caller hand-supervising it around Start.
Boot order: migrate → OnStart → Service.Start (registration order) → listen. Shutdown order: http.Shutdown → Service.Stop (reverse order) → OnShutdown →
drain Server.Go + ctx.GoBackground goroutines.
All of shutdown is bounded by Config.ShutdownTimeout.
type ServiceFunc ¶
ServiceFunc adapts a bare start function into a Service. Stop is a no-op, so the function is expected to honour cancellation of the ctx passed to Start (e.g. a loop that returns on ctx.Done()).
server.AddService(maniflex.ServiceFunc(func(ctx context.Context) error {
go poller.Run(ctx) // exits when ctx is cancelled at shutdown
return nil
}))
type SoftDeletable ¶
type SoftDeletable interface {
SoftDeleteConfig() SoftDeleteConfig
}
SoftDeletable is satisfied by models that declare their own soft-delete config.
type SoftDeleteConfig ¶
type SoftDeleteConfig struct {
Enabled bool
Field string // DB column name, e.g. "deleted_at"
FieldType SoftDeleteStyle // how to test "is deleted"
}
SoftDeleteConfig describes how a model handles soft deletion. When Enabled is false (default) hard-deletes are performed.
type SoftDeleteStyle ¶
type SoftDeleteStyle int
SoftDeleteStyle indicates how soft deletion is stored in the database.
const ( // SoftDeleteTimestamp stores a nullable timestamp; NULL means not deleted. SoftDeleteTimestamp SoftDeleteStyle = iota // SoftDeleteBool stores a boolean flag; false/0 means not deleted. SoftDeleteBool )
type SortExpr ¶
type SortExpr struct {
DBName string // DB column name
Direction SortDir // asc or desc
// IsLocale is true when the sort column is a LocaleString field and the
// sort should use a JSON path expression targeting LocaleKey.
IsLocale bool
LocaleKey string // e.g. "ar" — set by locale enrichment in the Deserialize step
// Nested sort — the sort name contained a "." referencing a BelongsTo
// relation, e.g. ?sort=vendor.name:asc. Mirrors the nested fields on
// FilterExpr; the query builder adds a LEFT JOIN and orders on the
// related table's column.
IsNested bool
RelationKey string // relation key / JOIN alias, e.g. "vendor"
RelationModel string // related model struct name, e.g. "Vendor"
RelationTable string // DB table of related model, e.g. "vendors"
RelationFK string // FK column on THIS table, e.g. "vendor_id"
NestedField string // DB column on the related table, e.g. "name"
}
SortExpr is a parsed sort instruction.
type StepRegistry ¶
type StepRegistry struct {
// contains filtered or unexported fields
}
StepRegistry holds all middlewares registered for one pipeline step. Obtain one from the Pipeline struct and call Register() on it.
func (*StepRegistry) Register ¶
func (s *StepRegistry) Register(fn MiddlewareFunc, opts ...MiddlewareOption)
Register adds a middleware to this pipeline step.
Without options the middleware applies to every model and operation. Combine options to narrow the scope:
// Run before the DB step for all POST /users requests
pipeline.DB.Register(hashPassword,
maniflex.ForModel("User"),
maniflex.ForOperation(maniflex.OpCreate),
)
// Run after the Response step for every request
pipeline.Response.Register(addCorpHeaders, maniflex.AtPosition(maniflex.After))
// Replace the default Service step for Post creates
pipeline.Service.Register(publishPost,
maniflex.ForModel("Post"),
maniflex.ForOperation(maniflex.OpCreate),
maniflex.AtPosition(maniflex.Replace),
)
type Tx ¶
type Tx interface {
// The same CRUD methods as DBAdapter, used when routing through a transaction.
// Typed-models contract (Phase 3): the returned any is always a *T for
// model.GoType; record passed to Create/Update is likewise a *T.
FindByID(ctx context.Context, model *ModelMeta, id string, q *QueryParams) (any, error)
FindMany(ctx context.Context, model *ModelMeta, q *QueryParams) ([]any, int64, error)
Create(ctx context.Context, model *ModelMeta, record any) (any, error)
Update(ctx context.Context, model *ModelMeta, id string, record any, present map[string]struct{}) (any, error)
Delete(ctx context.Context, model *ModelMeta, id string) error
// FindByIDForUpdate fetches the record and acquires a pessimistic row-level
// lock held until the transaction commits or rolls back.
// Postgres: appends FOR UPDATE to the SELECT.
// SQLite: behaves identically to FindByID — the lock is at the transaction
// level (BEGIN IMMEDIATE / _txlock=immediate DSN option).
FindByIDForUpdate(ctx context.Context, model *ModelMeta, id string) (any, error)
// Commit finalises the transaction. Returns an error if the commit fails.
Commit() error
// Rollback aborts the transaction. Returns nil if the transaction has
// already been committed (safe to defer unconditionally).
Rollback() error
}
Tx is a transaction handle. It exposes the same write operations as DBAdapter so middleware can use it identically to the adapter itself.
Obtain one via ctx.BeginTx() and store it on ctx.Tx:
tx, err := ctx.BeginTx(ctx.Ctx, nil)
if err != nil { ... }
ctx.Tx = tx
defer tx.Rollback() // no-op after Commit
The default DB step routes through ctx.Tx when it is non-nil, so all Create / Update / Delete calls within the same request will use the transaction.
func TxFromContext ¶
TxFromContext returns the active database transaction stored in ctx by WithTransaction, or nil when no transaction is active. Use this from packages (e.g. jobs/sql) that need to join the caller's transaction without holding a *ServerContext reference.
type UploadedFile ¶
type UploadedFile struct {
Filename string
ContentType string
Size int64
Reader io.ReadCloser
}
UploadedFile holds a parsed file from a multipart request, ready for processing by the Service step.
type WithDeletedAt ¶
type WithDeletedAt struct {
DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at" mfx:"readonly,filterable"`
}
WithDeletedAt enables deleted_at-style soft deletion.
func (WithDeletedAt) SoftDeleteConfig ¶
func (WithDeletedAt) SoftDeleteConfig() SoftDeleteConfig
type WithIsDeleted ¶
type WithIsDeleted struct {
IsDeleted bool `json:"is_deleted" db:"is_deleted" mfx:"readonly,filterable,default:false"`
}
WithIsDeleted enables is_deleted boolean-style soft deletion.
func (WithIsDeleted) SoftDeleteConfig ¶
func (WithIsDeleted) SoftDeleteConfig() SoftDeleteConfig
Source Files
¶
- action.go
- aggregate.go
- aggregate_endpoint.go
- asyncapi_gen.go
- asyncapi_handler.go
- asyncapi_types.go
- background.go
- batch.go
- cache.go
- computed.go
- config.go
- context.go
- cursor.go
- db.go
- encryption.go
- env.go
- export.go
- file_handler.go
- filter.go
- for_typed.go
- handler.go
- health.go
- locale.go
- lock_scope.go
- lock_when.go
- m2m.go
- middleware.go
- migrate.go
- model.go
- mount.go
- openapi_gen.go
- openapi_pipeline.go
- openapi_types.go
- pipeline.go
- query.go
- record_bridge.go
- recover.go
- recursive.go
- registry.go
- request_body.go
- router.go
- search.go
- server.go
- service.go
- steps.go
- storage.go
- tags.go
- transaction.go
- typed_body.go
- typed_crud.go
- utils.go
- versioning.go
Directories
¶
| Path | Synopsis |
|---|---|
|
admin
module
|
|
|
db
|
|
|
sqlcore
Package sqlcore provides a database/sql-backed DBAdapter that works with both PostgreSQL ($N placeholders) and SQLite (? placeholders).
|
Package sqlcore provides a database/sql-backed DBAdapter that works with both PostgreSQL ($N placeholders) and SQLite (? placeholders). |
|
postgres
module
|
|
|
sqlite
module
|
|
|
Package events provides a CloudEvents 1.0-aligned event bus for maniflex.
|
Package events provides a CloudEvents 1.0-aligned event bus for maniflex. |
|
cloudevents
Package cloudevents provides encoding and decoding helpers for CloudEvents 1.0.
|
Package cloudevents provides encoding and decoding helpers for CloudEvents 1.0. |
|
inproc
Package inproc provides an in-process event bus for maniflex.
|
Package inproc provides an in-process event bus for maniflex. |
|
outbox
Package outbox provides a transactional outbox publisher for maniflex events.
|
Package outbox provides a transactional outbox publisher for maniflex events. |
|
kafka
module
|
|
|
nats
module
|
|
|
rabbitmq
module
|
|
|
redis
module
|
|
|
Package jobs provides a durable, adapter-pluggable job queue for maniflex.
|
Package jobs provides a durable, adapter-pluggable job queue for maniflex. |
|
cron
Package cron provides a best-effort scheduled trigger for maniflex jobs.
|
Package cron provides a best-effort scheduled trigger for maniflex jobs. |
|
inproc
Package inproc provides an in-process job queue backed by a goroutine pool.
|
Package inproc provides an in-process job queue backed by a goroutine pool. |
|
maniflex
Package jobsmaniflex (imported as jobs/maniflex) integrates the jobs package with the maniflex model layer (3C.4).
|
Package jobsmaniflex (imported as jobs/maniflex) integrates the jobs package with the maniflex model layer (3C.4). |
|
sql
maniflex_glue.go wires maniflex.TxFromContext into the jobs/sql outbox path.
|
maniflex_glue.go wires maniflex.TxFromContext into the jobs/sql outbox path. |
|
redis
module
|
|
|
middleware
|
|
|
auth
Package auth provides authentication and authorisation middleware for maniflex.
|
Package auth provides authentication and authorisation middleware for maniflex. |
|
body
Package body provides Deserialize-step middleware for request body control.
|
Package body provides Deserialize-step middleware for request body control. |
|
db
Package db provides DB-step middleware for query control, tenancy, rate limiting, auditing, and cache invalidation.
|
Package db provides DB-step middleware for query control, tenancy, rate limiting, auditing, and cache invalidation. |
|
idempotency
Package idempotency provides a Deserialize-step middleware that makes POST requests safe to retry over flaky networks.
|
Package idempotency provides a Deserialize-step middleware that makes POST requests safe to retry over flaky networks. |
|
openapi
Package openapi provides OpenAPI pipeline middleware for enriching the auto-generated spec with security schemes, servers, examples, and extensions.
|
Package openapi provides OpenAPI pipeline middleware for enriching the auto-generated spec with security schemes, servers, examples, and extensions. |
|
response
Package response provides Response-step middleware for CORS, caching, field transforms, custom envelopes, and observability.
|
Package response provides Response-step middleware for CORS, caching, field transforms, custom envelopes, and observability. |
|
service
Package service provides Service-step middleware for common field transforms and computed-value injection that runs before the DB write.
|
Package service provides Service-step middleware for common field transforms and computed-value injection that runs before the DB write. |
|
validate
Package validate provides Validate-step middleware for advanced field rules that cannot be expressed with static mfx struct tags.
|
Package validate provides Validate-step middleware for advanced field rules that cannot be expressed with static mfx struct tags. |
|
workflow
Package workflow provides a Validate-step middleware that enforces permitted state transitions on a model.
|
Package workflow provides a Validate-step middleware that enforces permitted state transitions on a model. |
|
auth/redis
module
|
|
|
db/redis
module
|
|
|
service/bcrypt
module
|
|
|
pkg
|
|
|
encryption
Package encryption provides KeyProvider implementations for mfx:"encrypted" struct fields.
|
Package encryption provides KeyProvider implementations for mfx:"encrypted" struct fields. |
|
integration
Package integration provides building blocks for outbound integrations: HTTP callers with retry/backoff, periodic pollers, and signed-webhook receivers.
|
Package integration provides building blocks for outbound integrations: HTTP callers with retry/backoff, periodic pollers, and signed-webhook receivers. |
|
ledger
Package ledger provides double-entry accounting primitives for Maniflex applications.
|
Package ledger provides double-entry accounting primitives for Maniflex applications. |
|
money
Package money provides a fixed-precision monetary amount type for use as a model field.
|
Package money provides a fixed-precision monetary amount type for use as a model field. |
|
saga
Package saga provides a lightweight in-process saga coordinator for cross-domain atomic operations.
|
Package saga provides a lightweight in-process saga coordinator for cross-domain atomic operations. |
|
otel
module
|
|
|
Package realtime provides a WebSocket and SSE hub backed by events.Bus.
|
Package realtime provides a WebSocket and SSE hub backed by events.Bus. |
|
Package scheduled applies time-driven state transitions to registered models.
|
Package scheduled applies time-driven state transitions to registered models. |
|
jobsx
Package jobsx bridges a scheduled.Runner to the jobs system, enabling the durable/distributed sweep path without making the core scheduled package depend on jobs.
|
Package jobsx bridges a scheduled.Runner to the jobs system, enabling the durable/distributed sweep path without making the core scheduled package depend on jobs. |
|
Package storage provides file storage backends for maniflex.
|
Package storage provides file storage backends for maniflex. |
|
s3
module
|