Documentation
¶
Overview ¶
Package types defines the public contracts between the framework and business projects: the Model, Service, Database, Aggregator, Cache, RBAC, and Logger interfaces, the query building blocks they exchange (Filter, Order, Cursor, Column, aggregate terms), and the per-request ServiceContext.
Index ¶
- Constants
- Variables
- func RequestUserID(ctx context.Context) string
- type AggregateFn
- type AggregateOrder
- type AggregateTerm
- func AvgOf(column string) AggregateTerm
- func ByDayOf(column string) AggregateTerm
- func ByHourOf(column string) AggregateTerm
- func ByMonthOf(column string) AggregateTerm
- func Count() AggregateTerm
- func CountDistinctOf(column string) AggregateTerm
- func CountOf(column string) AggregateTerm
- func GroupOf(column string) AggregateTerm
- func MaxOf(column string) AggregateTerm
- func MinOf(column string) AggregateTerm
- func SumOf(column string) AggregateTerm
- func (t AggregateTerm) As(alias string) AggregateTerm
- func (t AggregateTerm) Asc() AggregateOrder
- func (t AggregateTerm) Desc() AggregateOrder
- func (t AggregateTerm) Eq(value any) Having
- func (t AggregateTerm) Gt(value any) Having
- func (t AggregateTerm) Gte(value any) Having
- func (t AggregateTerm) IsMeasure() bool
- func (t AggregateTerm) Lt(value any) Having
- func (t AggregateTerm) Lte(value any) Having
- func (t AggregateTerm) Ne(value any) Having
- func (t AggregateTerm) Where(filters ...Filter) AggregateTerm
- type Aggregator
- type AnyColumnRef
- type Assignment
- type Cache
- type Coder
- type Column
- func (c Column[T]) Asc() Order
- func (c Column[T]) Correlate(parent ColumnRef[T]) Filter
- func (c Column[T]) Count() AggregateTerm
- func (c Column[T]) CountDistinct() AggregateTerm
- func (c Column[T]) Desc() Order
- func (c Column[T]) EndsWith(value string) Filter
- func (c Column[T]) Eq(value T) Filter
- func (c Column[T]) Group() AggregateTerm
- func (c Column[T]) Gt(value T) Filter
- func (c Column[T]) Gte(value T) Filter
- func (c Column[T]) In(values ...T) Filter
- func (c Column[T]) IsNull() Filter
- func (c Column[T]) JSONContains(value string) Filter
- func (c Column[T]) Like(value string) Filter
- func (c Column[T]) Lt(value T) Filter
- func (c Column[T]) Lte(value T) Filter
- func (c Column[T]) Max() AggregateTerm
- func (c Column[T]) Min() AggregateTerm
- func (c Column[T]) Name() string
- func (c Column[T]) Ne(value T) Filter
- func (c Column[T]) NotIn(values ...T) Filter
- func (c Column[T]) NotLike(value string) Filter
- func (c Column[T]) NotNull() Filter
- func (c Column[T]) NotRegex(expr string) Filter
- func (c Column[T]) Regex(expr string) Filter
- func (c Column[T]) Set(value T) Assignment
- func (c Column[T]) StartsWith(value string) Filter
- type ColumnRef
- type ControllerConfig
- type Cursor
- type Database
- type DatabaseOption
- type Decision
- type ESDocumenter
- type Filter
- func FilterAnd(filters ...Filter) Filter
- func FilterCorrelate(column, parent string) Filter
- func FilterEndsWith(column, value string) Filter
- func FilterEq(column string, value any) Filter
- func FilterExists[C Model](filters ...Filter) Filter
- func FilterGt(column string, value any) Filter
- func FilterGte(column string, value any) Filter
- func FilterIn[T any](column string, values []T) Filter
- func FilterIsNull(column string) Filter
- func FilterJSONContains(column, value string) Filter
- func FilterLike(column, value string) Filter
- func FilterLt(column string, value any) Filter
- func FilterLte(column string, value any) Filter
- func FilterNe(column string, value any) Filter
- func FilterNotExists[C Model](filters ...Filter) Filter
- func FilterNotIn[T any](column string, values []T) Filter
- func FilterNotLike(column, value string) Filter
- func FilterNotNull(column string) Filter
- func FilterNotRegex(column, expr string) Filter
- func FilterOr(filters ...Filter) Filter
- func FilterRegex(column, expr string) Filter
- func FilterStartsWith(column, value string) Filter
- type FilterOp
- type Having
- type HavingOp
- type Logger
- type Model
- type Module
- type NumericColumn
- type Order
- type OrderDirection
- type Permission
- type QueryOptions
- type RBAC
- type Request
- type Response
- type SQLStatement
- type Service
- type ServiceContext
- func (sc *ServiceContext) ClientIP() string
- func (sc *ServiceContext) Cookie(name string) (string, error)
- func (sc *ServiceContext) Data(code int, contentType string, data []byte)
- func (sc *ServiceContext) Deadline() (time.Time, bool)
- func (sc *ServiceContext) Done() <-chan struct{}
- func (sc *ServiceContext) Err() error
- func (sc *ServiceContext) FormFile(name string) (*multipart.FileHeader, error)
- func (sc *ServiceContext) Host() string
- func (sc *ServiceContext) IsHTTPS() bool
- func (sc *ServiceContext) Method() string
- func (sc *ServiceContext) Param(key string) string
- func (sc *ServiceContext) Path() string
- func (sc *ServiceContext) Phase() consts.Phase
- func (sc *ServiceContext) PostForm(key string) string
- func (sc *ServiceContext) Query() url.Values
- func (sc *ServiceContext) RequiresAuth() bool
- func (sc *ServiceContext) Route() string
- func (sc *ServiceContext) SSE(fn func(conn *sse.Conn) error, opts ...sse.Option) error
- func (sc *ServiceContext) SessionID() string
- func (sc *ServiceContext) SetCookie(cookie *http.Cookie)
- func (sc *ServiceContext) TenantID() string
- func (sc *ServiceContext) TraceID() string
- func (sc *ServiceContext) UserAgent() string
- func (sc *ServiceContext) UserID() string
- func (sc *ServiceContext) Username() string
- func (sc *ServiceContext) Value(key any) any
- type StandardLogger
- type StructuredLogger
- type Subquery
- type TimeBucket
- type TimeColumn
- type ZapLogger
Examples ¶
Constants ¶
const DefaultCountAlias = "count"
DefaultCountAlias is the alias COUNT(*) projects under when the caller does not rename it. A column term defaults to its column name, but COUNT(*) names no column, so without a default of its own it would be the one term that always had to be renamed.
const FilterTimeLayout = "2006-01-02 15:04:05.999999999"
FilterTimeLayout is the canonical layout a time-typed filter value parsed from a URL is normalized to. The value travels as a string rather than a time.Time on purpose: binding a time.Time would let the driver re-render it in its own location, while the string pins the wall-clock time the parser resolved. The pinned wall clock is UTC, the one wall clock the framework stores on every dialect.
It is exported because the normalization is a contract of Filter, not a detail of the parser: a service reading a bound back would otherwise have to restate the layout, which no compiler could keep in sync with the parser. Read a bound with TimeValue rather than parsing with this layout directly.
Variables ¶
var ErrEntryNotFound = errors.New("cache entry not found")
ErrEntryNotFound is returned when a cache entry is not found.
var ErrTTLNotSupported = errors.New("cache backend does not support the requested ttl")
ErrTTLNotSupported is returned by Cache.Set when the backend cannot honor the requested ttl semantics, such as a per-entry lifetime on a backend without per-entry expiration.
Functions ¶
func RequestUserID ¶
RequestUserID reports the authenticated subject of the request ctx descends from, or "" when no request is behind it.
It exists for code that receives a plain context and still has to know who is acting — a model hook guarding an operation, like tenant.From for a model deriving its key. An empty answer means machinery rather than a person: seeding, a scheduled job, framework code. Inside a request it cannot be empty, because authorization refuses anonymous requests before any handler runs.
Types ¶
type AggregateFn ¶
type AggregateFn string
AggregateFn is the function applied to one projection term. The set is closed, so a projection can never carry SQL the way a free-form select string could: the renderer maps each constant to a fixed expression and rejects anything else.
const ( // AggregateNone marks a group key rather than a measure. A projection term // without an aggregate function is what the framework derives GROUP BY // from, so the SELECT and GROUP BY lists can never disagree. AggregateNone AggregateFn = "" AggregateCount AggregateFn = "COUNT" AggregateCountDistinct AggregateFn = "COUNT_DISTINCT" AggregateSum AggregateFn = "SUM" AggregateAvg AggregateFn = "AVG" AggregateMin AggregateFn = "MIN" AggregateMax AggregateFn = "MAX" )
func (AggregateFn) Valid ¶
func (f AggregateFn) Valid() bool
Valid reports whether the function is one this package defines. The renderer composes SQL from the constant, so a value from outside the set would reach the statement as text; the query builder rejects it instead.
type AggregateOrder ¶
type AggregateOrder struct {
Term AggregateTerm
Direction OrderDirection
}
AggregateOrder is one ORDER BY term of an aggregate query. Unlike Order it sorts by a projection term, which is what a TopN report ranks by.
type AggregateTerm ¶
type AggregateTerm struct {
// Fn is the aggregate function, or AggregateNone for a group key.
Fn AggregateFn
// Column is the snake case column name. It is empty only for COUNT(*).
Column string
// Bucket truncates a time group key. It is only meaningful when Fn is
// AggregateNone and the column is a time column.
Bucket TimeBucket
// Conditions restrict a measure to the rows matching them, rendering as a
// CASE expression inside the aggregate call. They reuse the query filter
// tree, so conditional aggregation needs no predicate language of its own.
Conditions []Filter
// Alias names the term in the SELECT list and binds it to a field of the
// result row. An empty alias defaults to the column name.
Alias string
}
AggregateTerm is one term of an aggregate projection: a group key when Fn is AggregateNone, a measure otherwise.
Terms are built through the generated column references (SampleCols.Amount.Sum()) or, for code that cannot name a concrete model, through the package-level string variants (SumOf("amount")). The typed path cannot express a function the column type does not support, because the generated reference does not carry the method; the string path names a column that is checked against the model schema when the query is built.
A term never holds SQL. Column names are quoted by the database layer, values bind as statement parameters, and Fn and Bucket come from closed sets.
func ByDayOf ¶
func ByDayOf(column string) AggregateTerm
func ByHourOf ¶
func ByHourOf(column string) AggregateTerm
ByHourOf, ByDayOf and ByMonthOf group a time column by a truncated bucket.
func ByMonthOf ¶
func ByMonthOf(column string) AggregateTerm
func Count ¶
func Count() AggregateTerm
Count counts rows: COUNT(*). It counts a row even when every column is NULL, which is what a plain row count means; use a column reference's Count for COUNT(column), which skips NULLs.
It projects as "count" unless renamed with As.
func CountDistinctOf ¶
func CountDistinctOf(column string) AggregateTerm
CountDistinctOf counts distinct non-NULL values of a column.
func CountOf ¶
func CountOf(column string) AggregateTerm
CountOf counts non-NULL values of a column.
func GroupOf ¶
func GroupOf(column string) AggregateTerm
GroupOf groups by the raw value of a column.
func (AggregateTerm) As ¶
func (t AggregateTerm) As(alias string) AggregateTerm
As renames the term in the SELECT list.
It is optional. Every term already carries a default alias — the column name for a column term, "count" for COUNT(*) — so a projection whose result row fields are named after the columns needs no As at all:
Select(SampleCols.TenantID.Group(), SampleCols.Amount.Sum())
// scans into struct{ TenantID string; Amount int64 }
Reach for As in the two cases the default cannot cover: when the result row field is named differently from the column, and when one projection carries two terms over the same column, whose default aliases would collide.
The alias belongs to the result contract rather than to the column, which is why it is applied here instead of being a parameter of the constructors.
func (AggregateTerm) Asc ¶
func (t AggregateTerm) Asc() AggregateOrder
Asc and Desc sort the result rows by this term. An output alias is legal in ORDER BY on every supported dialect, so these render as the alias.
func (AggregateTerm) Desc ¶
func (t AggregateTerm) Desc() AggregateOrder
func (AggregateTerm) Eq ¶
func (t AggregateTerm) Eq(value any) Having
Eq, Ne, Gt, Gte, Lt and Lte build a post-aggregation condition on the term. The value type is checked when the query is built, because an aggregate's value type follows its function rather than its column: COUNT always yields an integer, AVG a float, and SUM widens.
func (AggregateTerm) Gt ¶
func (t AggregateTerm) Gt(value any) Having
func (AggregateTerm) Gte ¶
func (t AggregateTerm) Gte(value any) Having
func (AggregateTerm) IsMeasure ¶
func (t AggregateTerm) IsMeasure() bool
IsMeasure reports whether the term is an aggregate rather than a group key.
func (AggregateTerm) Lt ¶
func (t AggregateTerm) Lt(value any) Having
func (AggregateTerm) Lte ¶
func (t AggregateTerm) Lte(value any) Having
func (AggregateTerm) Ne ¶
func (t AggregateTerm) Ne(value any) Having
func (AggregateTerm) Where ¶
func (t AggregateTerm) Where(filters ...Filter) AggregateTerm
Where restricts a measure to the rows matching filters, which is how a report projects several measures over different subsets in a single scan:
Count().Where(SampleCols.Status.Eq(StatusFailed)).As("failed")
// COUNT(CASE WHEN `status` = ? THEN 1 END) AS `failed`
The filters are the ordinary query filters, including nested groups, so the same fail-closed rules and the same renderer apply.
type Aggregator ¶
type Aggregator[M Model, R any] interface { // Select declares the projection. A term without an aggregate function is // a group key, and GROUP BY is derived from those keys, so the SELECT and // GROUP BY lists cannot disagree. At least one aggregate term is required. Select(terms ...AggregateTerm) Aggregator[M, R] // Where restricts the rows entering the aggregation, using the same filter // tree as WithQuery. Where(filters ...Filter) Aggregator[M, R] // Having restricts the produced groups by their measures. Having(conditions ...Having) Aggregator[M, R] // OrderBy sorts the result rows by a projection term. OrderBy(orders ...AggregateOrder) Aggregator[M, R] // Limit caps the number of result rows. Limit(n int) Aggregator[M, R] // Offset skips result rows, for paginating a grouped report. Offset(n int) Aggregator[M, R] // Scan runs the query and fills dest with one element per group. Scan(dest *[]R) error // ScanOne runs an ungrouped aggregation and fills dest with its single // row. It fails when the projection declares group keys. ScanOne(dest *R) error // CountGroups reports how many groups the query produces, which is the // total a paginated grouped report needs. OrderBy, Limit and Offset set on // the builder do not apply to it. CountGroups(count *int) error // WithDryRun builds the SQL without database I/O. An optional collector // receives the generated Query, Args, and RenderedSQL of the next // terminal operation instead of executing it. WithDryRun(collector ...*[]SQLStatement) Aggregator[M, R] }
Aggregator runs an analytical read over the table of M and scans the result rows into R. It is deliberately separate from Database[M]: an aggregate result is not a model row, so model hooks, association preloading and cursor pagination have nothing to act on and are absent here rather than present and inert.
Scoping comes from M — the table name, the soft-delete condition and the dialect — so an aggregate can never read rows a List on the same model hides. R is an ordinary struct the caller declares; its fields bind to the projection aliases, and a mismatch on either side is a build error rather than a silently zero column. A measure that can come back NULL — AVG, MIN or MAX without group keys, carrying conditions, or over a nullable column — must bind to a pointer or sql.Null field, which is again a build error rather than a zero on the report.
The entry point is the package-level database.Aggregate[M, R] rather than a method, because a Go method cannot introduce the result type parameter.
Row-level access rules are not inherited. A model's List gets its tenant or group scoping from the Filter service hook the controller runs; an aggregate is called straight from service code, so those hooks never run and every scoping condition has to be passed to Where explicitly. Forgetting one aggregates across tenants without any sign that it did.
A builder is a specification, not a live statement: it can be read more than once, and each terminal renders the spec afresh, taking only the parts that are meaningful to it — Scan and ScanOne render everything, CountGroups ignores OrderBy, Limit and Offset because none of them changes how many groups exist. That is what makes the paginated-report idiom safe — Scan for the page, then CountGroups for the total, off the same builder, with the pagination never skewing the count.
Example:
type tenantTotal struct {
TenantID string
Total int64
// ClosedAt is nullable on Sample, so MAX over it can be NULL and
// needs a field that can hold NULL.
LastClosed *time.Time
}
total := SampleCols.Amount.Sum().As("total")
rows := make([]tenantTotal, 0)
err := database.Aggregate[*Sample, tenantTotal](ctx).
Select(SampleCols.TenantID.Group(), total,
SampleCols.ClosedAt.Max().As("last_closed")).
Where(SampleCols.Status.Eq(StatusDone)).
Having(total.Gte(1000)).
OrderBy(total.Desc()).
Limit(10).
Scan(&rows)
type AnyColumnRef ¶
type AnyColumnRef interface {
// Name returns the database column name resolved by gorm.
Name() string
// contains filtered or unexported methods
}
AnyColumnRef is the type-erased view of every generated column reference, for options that take a heterogeneous column list: WithSelect accepts columns of different Go types in one call, which the parameterized ColumnRef cannot express. The unexported method keeps the set of implementations closed to this package, so a stray type that happens to carry a Name method cannot slip into a column list.
type Assignment ¶
Assignment is one column-value write, the unit UpdateByID accepts. Service code should build assignments through the generated column references (SampleCols.Status.Set(v)), whose typed front end stops a wrong-typed value or a misspelled column at compile time. The Assign constructor takes a plain column name and exists for code that cannot reference a concrete model: framework internals and dynamic column loops.
An Assignment never holds SQL. Column names are quoted by the database layer and values bind as statement parameters.
func Assign ¶
func Assign(column string, value any) Assignment
Assign builds an assignment of value to the named database column.
type Cache ¶
type Cache[T any] interface { // Get retrieves a value from the cache by key. // Returns ErrEntryNotFound if the key does not exist. Get(ctx context.Context, key string) (T, error) // Set stores a value in the cache with the specified TTL. Set(ctx context.Context, key string, value T, ttl time.Duration) error // Delete removes a key from the cache. // Deleting a key that does not exist is not an error. Delete(ctx context.Context, key string) error // Exists checks if a key exists in the cache. // Returns true if the key exists, false otherwise. Exists(ctx context.Context, key string) bool }
Cache provides a typed key/value cache abstraction.
Type Parameters:
- T: Cached value type
Error Handling:
- Get returns ErrEntryNotFound when the key does not exist.
- Delete is idempotent: deleting a missing key returns nil.
- Set returns ErrTTLNotSupported when the backend cannot honor the requested ttl semantics.
TTL Semantics:
- ttl == 0 means the entry never expires.
- ttl > 0 sets a per-entry lifetime; backends that cannot honor it must return ErrTTLNotSupported instead of silently mis-honoring the request.
- ttl < 0 is invalid and returns an error.
The ctx parameter carries cancellation and tracing for backends that talk to remote systems; in-memory backends may ignore it. A nil ctx is treated as context.Background(), so callers never need to normalize it themselves.
type Column ¶
type Column[T any] struct { // contains filtered or unexported fields }
Column is a typed reference to a database column, generated per model by gg gen. T is the Go type of the column, so a filter built through a Column cannot name a column that does not exist nor bind a value of the wrong type: both mistakes stop at compile time instead of surfacing as a SQL error or a silently wrong result set.
The methods are typed front ends for the FilterXxx, Asc and Desc constructors and produce exactly the same Filter and Order values. Code that cannot reference a concrete model (generic helpers, framework internals, URL parsing) keeps using those constructors with a string column name.
Columns whose Go type is numeric or time.Time are generated as NumericColumn or TimeColumn instead, which embed this type and add the aggregate methods that are only meaningful there.
func NewColumn ¶
NewColumn returns a typed reference to the named database column. gg gen emits the calls in each model's generated file; handwritten code that cannot reference a generated Cols var should keep using the FilterXxx, Asc and Desc constructors with a plain column name instead of minting references. The name is unexported so a shared reference cannot be repointed at another column after construction.
An empty name panics: references are constructed while generated code initializes its package-level Cols vars, so a nameless one must not survive process startup.
func (Column[T]) Correlate ¶
Correlate ties the column, on the related model a subquery reads, to parent, a column of the query enclosing that subquery; see FilterCorrelate. Both must be columns of the same Go type. A nil parent leaves the outer side empty, and the predicate then fails closed.
func (Column[T]) Count ¶
func (c Column[T]) Count() AggregateTerm
Count counts the rows whose value of this column is not NULL. Use the package-level Count for COUNT(*), which counts every row.
func (Column[T]) CountDistinct ¶
func (c Column[T]) CountDistinct() AggregateTerm
CountDistinct counts the distinct non-NULL values of this column.
func (Column[T]) Group ¶
func (c Column[T]) Group() AggregateTerm
Group makes this column a group key of the projection. The framework derives GROUP BY from the group keys, so a projection cannot disagree with its own GROUP BY list.
func (Column[T]) In ¶
In matches rows where the column is one of values. Calling it without any value matches nothing, mirroring SQL list semantics.
func (Column[T]) JSONContains ¶
JSONContains matches rows whose JSON array column contains value.
func (Column[T]) Like ¶
Like matches rows where the column contains value as a substring. The pattern is a string on every column type, because substring matching runs against the database's string rendering of the value.
func (Column[T]) Max ¶
func (c Column[T]) Max() AggregateTerm
Max returns the largest value of this column. The NULL rules match Min.
func (Column[T]) Min ¶
func (c Column[T]) Min() AggregateTerm
Min returns the smallest value of this column. It yields NULL for a group with no non-NULL value, so the result row field must be a pointer.
func (Column[T]) Name ¶
Name returns the database column name resolved by gorm. It is also what the order and cursor constructors taking a plain column name expect.
func (Column[T]) NotIn ¶
NotIn matches rows where the column is none of values. Calling it without any value matches nothing; it does not mean "exclude nothing".
func (Column[T]) NotLike ¶
NotLike matches rows where the column does not contain value as a substring.
func (Column[T]) NotRegex ¶
NotRegex matches rows where the column does not match the regular expression expr.
func (Column[T]) Set ¶
func (c Column[T]) Set(value T) Assignment
Set assigns value to the column, the unit UpdateByID accepts. The value is typed by the column, so assigning a wrong-typed value or naming a column the model does not have fails to compile.
func (Column[T]) StartsWith ¶
StartsWith matches rows where the column starts with value.
type ColumnRef ¶
type ColumnRef[T any] interface { AnyColumnRef // contains filtered or unexported methods }
ColumnRef is the shared typed view of every generated column reference. Helpers that accept a column take this interface rather than a concrete struct, because embedding is not subtyping in Go: NumericColumn[T] cannot be passed where Column[T] is expected, so a helper typed on the struct would reject exactly the numeric and time columns it is most often used with.
The type parameter is load-bearing. sealedColumn mentions T, so two column references only satisfy the same ColumnRef[T] when their Go types match, which is what makes correlating a string column with an integer column fail to compile. The method is also unexported, so the set of implementations stays closed to this package.
type ControllerConfig ¶
type ControllerConfig[M Model] struct { // ParamName names the route parameter that carries the resource ID. ParamName string // Route is the raw route string the handler is registered under. Controller // factories derive the service registry key from it, so it must match the // route passed to the corresponding service.Register call. router.Register // fills it in automatically; an empty route resolves no service and the // handler falls back to the no-op default service. Route string }
ControllerConfig customizes how router.Register builds an internal handler for a route. It is the public configuration surface for controller behavior; the concrete controller handlers and their runtime state remain framework-owned.
type Cursor ¶
type Cursor struct {
// Order is the feed's stable ordering. An empty column falls back to the
// primary key in the database layer.
Order Order
// Value is the boundary row's column value. An empty value disables
// cursor pagination, which makes a zero Cursor a no-op.
Value string
// Backward travels against Order instead of along it, which is what a
// request for the previous page means.
Backward bool
}
Cursor is where a cursor-paginated read starts and which way it goes: the feed's stable ordering, the boundary row, and whether the read travels along that ordering or back down it.
This is the database argument. model.Cursor is the separate struct a model embeds to opt in to the cursor URL parameters that produce it.
Traveling backward reverses both the boundary comparison and the ORDER BY, and List reverses the returned rows afterwards, so a backward page comes back in the feed's own order rather than upside down:
feed ASC, forward -> column > value, ORDER BY column ASC feed ASC, backward -> column < value, ORDER BY column DESC, rows reversed feed DESC, forward -> column < value, ORDER BY column DESC feed DESC, backward -> column > value, ORDER BY column ASC, rows reversed
URL-driven cursors always page an ascending feed; a descending feed is a service-side cursor, built with CursorForward on a Desc order.
func CursorBackward ¶
CursorBackward pages against order, starting just before value.
func CursorForward ¶
CursorForward pages along order, starting just past value.
type Database ¶
type Database[M Model] interface { // Create inserts one or more records (pure INSERT), setting framework IDs // and forcing created_at/updated_at to now. A primary or unique key // collision fails with database.ErrDuplicatedKey instead of updating the // existing row. Create(objs ...M) error // Delete removes one or more records using WithPurge, the model Purge setting, or soft delete by default. Delete(objs ...M) error // Update saves one or more full model values by primary key (pure UPDATE, // zero values included). Objects without an ID fail with // database.ErrIDRequired; records without a live row fail with // database.ErrRecordNotFound. created_at/created_by/deleted_at are never // written; updated_at is always refreshed by the framework. Update(objs ...M) error // Upsert inserts records or, on any unique-key collision, overwrites the // conflicting row (INSERT ... ON DUPLICATE KEY UPDATE). It runs no model // hooks and re-syncs caller objects with the persisted rows; reserve it // for deliberate merge writes such as imports and sync jobs. Upsert(objs ...M) error // UpdateByID updates database columns of a record by its ID in one // UPDATE statement, without running model hooks. Assignments come from // the generated column references (SampleCols.Status.Set(v)) or the // Assign constructor for dynamic columns; at least one is required, and // empty columns, nil values and a column assigned twice are rejected. UpdateByID(id string, assignments ...Assignment) error // List retrieves multiple records matching the query conditions. // dest must be a non-nil pointer to a slice; the slice value itself may be // nil or preallocated with make. List fully replaces the slice contents with // the query result: any pre-existing elements are discarded, never merged or // appended to. After a successful call len(*dest) equals the number of rows // returned, so a "dirty" dest does not leak stale rows into the result. List(dest *[]M) error // Get retrieves a single record by its ID. // The destination must be a non-nil pointer matching M. When M is *T, // both &value and new(T) are valid destinations; a nil *T returns ErrNilDest. // Get returns database.ErrRecordNotFound when no matching record exists. Get(dest M, id string) error // First retrieves the first record matching the current query conditions. // First returns database.ErrRecordNotFound when no matching record exists. First(dest M) error // Last retrieves the last record matching the current query conditions. // Last returns database.ErrRecordNotFound when no matching record exists. Last(dest M) error // Take retrieves the first record in no specified order. // Take returns database.ErrRecordNotFound when no matching record exists. Take(dest M) error // Count returns the total number of records matching the query conditions. Count(*int) error DatabaseOption[M] }
Database defines the model-scoped database operation contract. It provides CRUD operations, query builders, transactions, and optional dry-run behavior for a single Model type.
Type Parameters:
- M: Model type that implements Model interface
The interface embeds DatabaseOption[M] to provide chainable query building. A chain is expected to end with one terminal operation, such as Create, List, Get, or Count.
Implementations share an underlying GORM session. Call database.Database[M](ctx) again for each independent operation chain. Keeping the returned value in a variable and running another independent operation on it (for example, List then Get or Update) is incorrect usage; see database.Database.
Transactions are started with the package-level database.Transaction function; the context it passes to fn makes every chain started from that context join the transaction automatically.
type DatabaseOption ¶
type DatabaseOption[M Model] interface { // WithQuery adds query conditions from model fields or raw SQL configuration. WithQuery(query M, opts ...QueryOptions) Database[M] // WithCursor enables cursor-based pagination for List operations. WithCursor(cursor Cursor) Database[M] // WithSelect specifies columns for SELECT and Update column selection // where supported, through the generated column references. WithSelect(columns ...AnyColumnRef) Database[M] // WithLock adds row-level locking to SELECT queries (must be used within a transaction). WithLock(mode ...consts.LockMode) Database[M] // WithBatchSize sets the batch size for Create, Update, and Delete. WithBatchSize(size int) Database[M] // WithPagination applies pagination parameters (page, size) to the query. WithPagination(page, size int) Database[M] // WithLimit restricts the number of returned records for read operations. WithLimit(limit int) Database[M] // WithOffset skips records before returning read operation results. WithOffset(offset int) Database[M] // WithOrder adds ORDER BY terms to sort query results. WithOrder(orders ...Order) Database[M] // WithExpand enables eager loading of specified associations. WithExpand(expand []string, orders ...Order) Database[M] // WithPurge controls whether Delete permanently removes records instead of soft deleting them. WithPurge(...bool) Database[M] // WithDeleted includes soft-deleted records in read operations (List, Get, // First, Last, Take, Count). Only the soft-delete condition is lifted; // combining it with a write operation or Cleanup fails the chain. WithDeleted() Database[M] // WithReplica routes this read to a configured read replica — // WithReplica(false) forces the primary instead, overriding a model's // PreferReplica default. Reads only; a write operation fails the chain. WithReplica(prefer ...bool) Database[M] // WithDryRun builds SQL without database I/O, framework hooks, cache // mutation, or object field filling. An optional collector receives the // generated Query, Args, and RenderedSQL of the next terminal operation. WithDryRun(collector ...*[]SQLStatement) Database[M] // WithoutHook disables model hooks for the operation. WithoutHook() Database[M] }
DatabaseOption provides chainable options for a single Database operation chain. Options apply to the next terminal operation and are reset afterward. Start a new chain with database.Database[M](ctx) for each independent operation.
type Decision ¶
type Decision struct {
Allowed bool
Source consts.GrantSource
Reason consts.DenyReason
MatchedRule []string
}
Decision is the outcome of one authorization check.
Source names the strongest rule that allowed the request and is empty on a denial, because a denial has no granting rule. MatchedRule is the policy row that allowed it, and is nil unless Source names a policy: the rules that allow without consulting one leave the engine free to report an unrelated row, which would read as the reason for access while being nothing of the kind.
Reason is the mirror of Source and is set only on a denial, where naming a rule is not possible and what an operator needs instead is which step is missing. It is empty when the implementation could not tell, so an empty reason on a denial means unknown rather than none.
type ESDocumenter ¶
type ESDocumenter interface {
// Document returns a map representing an Elasticsearch document.
// The returned map should contain all fields to be indexed, where:
// - keys are field names (string type)
// - values are field values (any type)
//
// Implementation notes:
// 1. The returned map should only contain JSON-serializable values.
// 2. Field names should match those defined in the Elasticsearch mapping.
// 3. Complex types (like nested objects or arrays) should be correctly
// represented in the returned map.
//
// Example:
// return map[string]any{
// "id": "1234",
// "title": "Sample Document",
// "tags": []string{"tag1", "tag2"},
// }
Document() map[string]any
// GetID returns a string that uniquely identifies the document.
// This ID is typically used as the Elasticsearch document ID.
//
// Implementation notes:
// 1. The ID should be unique within the index.
// 2. If no custom ID is needed, consider returning an empty string
// to let Elasticsearch auto-generate an ID.
// 3. The ID should be a string, even if it's originally a numeric value.
//
// Example:
// return "user_12345"
GetID() string
}
ESDocumenter represents a document that can be indexed into Elasticsearch. Types implementing this interface should be able to convert themselves into a document format suitable for Elasticsearch indexing.
type Filter ¶
Filter is one field-level filter to apply as an AND condition. Column must already be validated against the model's queryable columns by the producer (the List controller validates URL input; service code passing filters directly carries the same responsibility). Value holds a normalized typed value and is always bound as a statement parameter:
- FilterOpIn and FilterOpNotIn require a slice or array value.
- FilterOpIsNull requires a bool value.
- FilterOpLike, FilterOpNotLike, FilterOpStartsWith, FilterOpEndsWith, FilterOpRegex, FilterOpNotRegex, and FilterOpJSONContains require a string value.
- FilterOpOr and FilterOpAnd require a non-empty []Filter value and carry no column: they group their children instead of naming one themselves.
- FilterOpExists requires a Subquery value and carries no column; see FilterExists.
- FilterOpCorrelate requires a string value naming the enclosing query's column and only renders inside a subquery; see FilterCorrelate.
- The comparison operators take a scalar value (string, numeric, time.Time); slices, arrays, and nil are rejected.
A value that violates these rules fails closed in the database layer. Service code should build filters with the FilterEq/FilterIn/... helper constructors: their signatures enforce the value shape at compile time.
func FilterAnd ¶
FilterAnd groups filters that are AND-combined with each other. Filters are already AND-combined at the top level, so the group exists to nest an AND inside an OR group:
Filters: []types.Filter{
types.FilterEq("tenant_id", tenant),
types.FilterOr(
types.FilterAnd(
types.FilterEq("kind", KindPrimary),
types.FilterEq("status", StatusDone),
),
types.FilterAnd(
types.FilterEq("kind", KindSecondary),
types.FilterEq("status", StatusPending),
),
),
}
// WHERE tenant_id = ?
// AND ((kind = ? AND status = ?) OR (kind = ? AND status = ?))
A group with no children fails closed.
func FilterCorrelate ¶
FilterCorrelate is the predicate that ties a subquery to the query around it: column, on the related model the subquery reads, equals parent, a column of the enclosing query's model. It only means something inside FilterExists or FilterNotExists, where it renders as `child_table.column = outer_table.parent`; at the top level of a query there is nothing to tie to and it fails closed, as does an empty name on either side or a name the related or the enclosing model does not have. Several of them express a composite key, and one inside a FilterOr group matches on any of its pairs. Column.Correlate is the typed front end that keeps the two columns of the same Go type.
func FilterEndsWith ¶
FilterEndsWith matches rows where column ends with value; value is escaped and matches literally.
func FilterExists ¶
FilterExists matches rows of the queried model that have at least one related row in C satisfying filters. Correlate predicates tie the related rows to the queried row, one per column pair, next to the ordinary conditions narrowing them:
types.FilterExists[*Item](
ItemCols.SampleID.Correlate(SampleCols.ID),
ItemCols.Status.Eq(StatusDone))
// EXISTS (SELECT 1 FROM `items`
// WHERE `items`.`sample_id` = `samples`.`id`
// AND `items`.`status` = ? AND `items`.`deleted_at` IS NULL)
A composite key is just more pairs, rendered in the order given:
types.FilterExists[*Item](
ItemCols.TenantID.Correlate(SampleCols.TenantID),
ItemCols.SampleID.Correlate(SampleCols.ID),
ItemCols.Status.Eq(StatusDone))
The table names come from C and from the queried model, so a column reference never has to carry a table name. A subquery without any FilterCorrelate fails closed rather than matching every row: nothing to correlate on is a mistake, not a request for a cross join.
It is an ordinary Filter, so List, Count, Export and Aggregate all accept it; it is service-only and has no URL spelling, because a client-supplied subquery is an unbounded read of a table the endpoint never named.
func FilterIn ¶
FilterIn matches rows where column is one of values. The slice is bound as a whole; an empty slice matches nothing.
func FilterIsNull ¶
FilterIsNull matches rows whose column is NULL.
func FilterJSONContains ¶
FilterJSONContains matches rows whose JSON array column contains value as a member.
func FilterLike ¶
FilterLike matches rows where column contains value as a substring; value is escaped and matches literally.
func FilterNotExists ¶
FilterNotExists matches rows that have no related row in C satisfying filters. Note that it is not the negation of a filtered FilterExists over the same rows: a row whose related rows all fail filters matches, and so does a row with no related rows at all. A subquery without any FilterCorrelate fails closed here as well: negating "match nothing" would otherwise widen into "match everything".
func FilterNotIn ¶
FilterNotIn matches rows where column is none of values. The slice is bound as a whole; an empty slice matches nothing (SQL NOT IN over an empty list never holds), it does not mean "exclude nothing".
func FilterNotLike ¶
FilterNotLike matches rows where column does not contain value as a substring; value is escaped and matches literally.
func FilterNotNull ¶
FilterNotNull matches rows whose column is not NULL.
func FilterNotRegex ¶
FilterNotRegex matches rows where column does not match the regular expression expr.
func FilterOr ¶
FilterOr groups filters that are OR-combined with each other. The group as a whole stays AND-combined with every other condition of the query, so a mandatory condition such as tenant scoping can never be absorbed into the alternatives:
Filters: []types.Filter{
types.FilterEq("tenant_id", tenant),
types.FilterOr(
types.FilterLike("name", keyword),
types.FilterLike("code", keyword),
),
}
// WHERE tenant_id = ? AND (name LIKE ? OR code LIKE ?)
Children may themselves be groups, which is how nesting is expressed; see FilterAnd for the "(a AND b) OR (c AND d)" shape. A group with no children fails closed.
func FilterRegex ¶
FilterRegex matches rows where column matches the regular expression expr (dialect-aware REGEXP).
func FilterStartsWith ¶
FilterStartsWith matches rows where column starts with value; value is escaped and matches literally, and the prefix form can use an index.
func (Filter) TimeValue ¶
TimeValue returns the filter's value as a time, which is how a service reads back a range it did not build itself, such as one parsed from a request.
Both value shapes a time bound can have are accepted: the canonical string a URL-parsed filter carries, and the time.Time a caller passes to the comparison constructors directly. It reports false for a value that is neither, including a malformed string, so a caller that must distinguish "no bound" from "some other value" can.
type FilterOp ¶
type FilterOp string
FilterOp is a field-level filter operator applied by WithQuery as an additional AND condition. Operators never widen a query: unknown values are rejected during parsing, and the database layer fails closed on conditions it does not recognize.
Operators come in two tiers, and the split is load-bearing:
- URL-exposed operators are registered in the filterOps parse map and carried by the List/Export query parameter syntax "field[op]=value"; FilterOps returns them for API documentation.
- Service-only operators exist as constants and execute in the database layer, but are intentionally absent from the parse map: they never validate column types or values at the URL boundary, so exposing one requires adding that validation first, not just registering the name.
const ( FilterOpEq FilterOp = "eq" // equal: column = value FilterOpNe FilterOp = "ne" // not equal: column <> value FilterOpGt FilterOp = "gt" // greater than: column > value FilterOpGte FilterOp = "gte" // greater than or equal: column >= value FilterOpLt FilterOp = "lt" // less than: column < value FilterOpLte FilterOp = "lte" // less than or equal: column <= value FilterOpIn FilterOp = "in" // set membership: column IN (comma-separated values) FilterOpNotIn FilterOp = "notin" // set exclusion: column NOT IN (comma-separated values) FilterOpLike FilterOp = "like" // substring match: column LIKE %value% FilterOpNotLike FilterOp = "notlike" // substring exclusion: column NOT LIKE %value% FilterOpStartsWith FilterOp = "startswith" // prefix match: column LIKE value% (can use an index) FilterOpEndsWith FilterOp = "endswith" // suffix match: column LIKE %value FilterOpIsNull FilterOp = "isnull" // null check: value true means IS NULL, false means IS NOT NULL )
URL-exposed operators.
const ( FilterOpRegex FilterOp = "regex" // regular expression match: column REGEXP value (dialect-aware) FilterOpNotRegex FilterOp = "notregex" // regular expression exclusion: NOT (column REGEXP value) FilterOpJSONContains FilterOp = "jsoncontains" // JSON array membership: value is a member of the JSON array column FilterOpOr FilterOp = "or" // group: the []Filter value is OR-combined, the group itself AND-combined FilterOpAnd FilterOp = "and" // group: the []Filter value is AND-combined, for nesting inside an OR group FilterOpExists FilterOp = "exists" // correlated subquery: the Subquery value becomes EXISTS or NOT EXISTS FilterOpCorrelate FilterOp = "correlate" // inside a subquery: the column equals the enclosing query's column named by the string value )
Service-only operators: for service code building Filters directly, reusable and injection-safe alternatives to raw SQL fragments.
func FilterOps ¶
func FilterOps() []FilterOp
FilterOps returns every URL-exposed operator in a stable order, for API documentation surfaces such as the generated OpenAPI parameter notes. Service-only operators are excluded on purpose: they are not part of the URL contract.
func ParseFilterOp ¶
ParseFilterOp converts an operator token from a "field[op]" query key into a FilterOp, reporting whether the token is a known operator.
type Having ¶
type Having struct {
Term AggregateTerm
Op HavingOp
Value any
}
Having is one post-aggregation condition. It carries the term itself rather than an alias string, which has two consequences: a condition can never name a measure the projection did not declare, and the renderer can emit the full expression instead of the alias, which is required because PostgreSQL and SQL Server do not accept an output alias in HAVING.
type HavingOp ¶
type HavingOp string
HavingOp is a comparison applied to an aggregated value. Only the six orderings exist: the pattern and set operators of FilterOp have no meaning over a measure.
type Logger ¶
type Logger interface {
With(fields ...string) Logger
WithContext(context.Context, consts.Phase) Logger
StandardLogger
StructuredLogger
ZapLogger
}
Logger combines plain, sugared structured, and typed zap logging methods. With attaches string key/value fields; WithContext derives a logger carrying request metadata fields.
type Model ¶
type Model interface {
TableName() string // TableName returns the explicit table name; gorm's Tabler reads the same method.
GetID() string // GetID returns the string form of the id, or "" when the id is unset.
SetID(id ...string) // SetID sets the id when unset; Base generates a UUID without an argument while AutoBase leaves generation to the database.
ClearID() // ClearID always set the id to empty.
GetCreatedBy() string
GetUpdatedBy() string
GetCreatedAt() time.Time
GetUpdatedAt() time.Time
SetCreatedBy(string)
SetUpdatedBy(string)
SetCreatedAt(time.Time)
SetUpdatedAt(time.Time)
Expands() []string // Expands returns association paths that should be preloaded by default.
Purge() bool // Purge indicates whether to permanently delete records (hard delete). Default is false (soft delete).
CreateBefore(context.Context) error
CreateAfter(context.Context) error
DeleteBefore(context.Context) error
DeleteAfter(context.Context) error
UpdateBefore(context.Context) error
UpdateAfter(context.Context) error
ListBefore(context.Context) error
ListAfter(context.Context) error
GetBefore(context.Context) error
GetAfter(context.Context) error
}
Model defines the framework contract for database-backed and action models. Typical database resources embed model.Base (UUIDv7 string primary key) or model.AutoBase (auto-increment integer primary key). Action-only models may use model.Empty when they do not represent persistent rows.
Type Requirements:
- Must be a pointer to struct (e.g., *User)
- Database resources should expose an ID primary key through GetID/SetID/ClearID
- Database resources must override TableName with an explicit non-empty name: gorm's Tabler reads the same method, and the base default "" is rejected at table preparation and inside gg migrate
- Hooks should be idempotent enough to run as part of framework CRUD phases
type Module ¶
type Module[M Model, REQ Request, RSP Response] interface { // Service returns the service instance that handles business logic for this module. Service() Service[M, REQ, RSP] // Route returns the base API path for this module's endpoints. Route() string // Pub determines whether the API endpoints are public or require authentication. Pub() bool // Param returns the URL parameter name used for resource identification. Param() string }
Module describes a registered API module: route metadata, auth exposure, resource parameter name, and the service implementation used by controllers.
Type Parameters:
- M: Model type that implements Model interface
- REQ: Request type for API operations
- RSP: Response type for API operations
type NumericColumn ¶
NumericColumn is the reference generated for a column whose Go type is numeric. It embeds Column, so every filter and order stays available, and adds the aggregate functions that only make sense over a number.
The specialization exists because of how the mistake fails, not because of tidiness: SUM over a text column returns 0 with a warning on MySQL and SQLite, so it surfaces as a plausible-looking wrong number on a dashboard rather than as an error. Functions whose misuse is merely useless rather than silently wrong stay on Column.
func NewNumericColumn ¶
func NewNumericColumn[T any](name string) NumericColumn[T]
NewNumericColumn returns the numeric reference to the named database column, carrying Sum and Avg on top of everything Column has.
func (NumericColumn[T]) Avg ¶
func (c NumericColumn[T]) Avg() AggregateTerm
Avg averages this column. It yields NULL for a group with no non-NULL value and is never coalesced, because a zero average and no data are different answers; the result row field must be a pointer.
func (NumericColumn[T]) Sum ¶
func (c NumericColumn[T]) Sum() AggregateTerm
Sum adds up this column. The renderer wraps it in COALESCE(..., 0) so an empty group sums to zero rather than scanning NULL into the result row.
type Order ¶
type Order struct {
Column string
Direction OrderDirection
}
Order is one ORDER BY term: a column and the direction to sort it by. Column must already be validated against the model's queryable columns by the producer (the List controller validates URL input; service code passing orders directly carries the same responsibility). An Order with an empty column is skipped rather than rendered.
Service code should build orders through the generated column references (SampleCols.CreatedAt.Desc()), which cannot name a column the model does not have. The Asc and Desc constructors take a plain column name and exist for code that cannot reference a concrete model: generic helpers, framework internals, and URL parsing.
type OrderDirection ¶
type OrderDirection string
OrderDirection is the sort direction of one ORDER BY term. The set is closed, so a direction can never carry SQL the way a free-form order string could.
const ( // OrderAsc sorts ascending. The zero OrderDirection is also read as // ascending, matching SQL's own default for an ORDER BY term. OrderAsc OrderDirection = "ASC" // OrderDesc sorts descending. OrderDesc OrderDirection = "DESC" )
func (OrderDirection) Flip ¶
func (d OrderDirection) Flip() OrderDirection
Flip returns the opposite direction. Cursor pagination uses it to read a feed backwards: traveling against the feed reverses both the boundary comparison and the ORDER BY.
func (OrderDirection) Valid ¶
func (d OrderDirection) Valid() bool
Valid reports whether the direction is one this package defines. A value from outside the set would otherwise fall through to ascending and sort a report the opposite of what the caller asked for.
type Permission ¶
type Permission struct {
// Object is the protected resource, usually a route path template such as
// /api/things/{id}.
Object string
// Action is the operation on that object, usually an HTTP method.
Action string
}
Permission is one operation a role is allowed to perform on one object. It is the unit the whole-set replacement methods on RBAC take, so a caller states a role's permissions as a set rather than as a sequence of grants.
type QueryOptions ¶
type QueryOptions struct {
// AllowEmpty allows a query without any condition to match all records.
// By default a nil model, a zero-value model, or all-empty field values
// add the "1 = 0" safety condition instead, so a forgotten filter cannot
// return or delete the whole table. RawQuery and Filters count as
// real conditions and disable the safety check on their own.
AllowEmpty bool
// RawQuery is a raw parameterized SQL fragment added as an extra WHERE
// condition. It works with a nil model and combines with model-field
// conditions otherwise.
RawQuery string
// RawQueryArgs are the values bound to the RawQuery placeholders.
RawQueryArgs []any
// PresentFields marks columns whose filter values were explicitly provided
// by the caller, keyed by snake case column name. Query construction treats
// zero values (false, 0) of these columns as real conditions instead of
// dropping them as unset, so a filter like "enabled=false" works. Columns
// not listed here keep the default zero-value skip.
PresentFields map[string]struct{}
// Filters are field-level operator filters ("field[op]=value"). They
// apply in every WithQuery path, including nil/empty model queries, so
// List and Count stay consistent. A condition with an unknown operator
// or empty column fails closed: query construction adds "1 = 0" instead
// of dropping it.
Filters []Filter
}
QueryOptions tunes how WithQuery turns a model value into WHERE conditions. Every condition it produces is AND-combined; the zero value means exact matching with the empty-query safety check enabled. See the WithQuery method for usage examples.
type RBAC ¶
type RBAC interface {
// Authorize reports whether subject may perform action on object inside
// tenant, and what allowed it.
//
// Implementations should treat tenant as the authorization domain, subject as
// the authenticated identity, object as the protected route or resource, and
// action as the operation being checked, such as an HTTP method.
//
// The reason is answered alongside the decision rather than by a second
// method. Deriving it costs a handful of allocations against the thousands
// the decision itself takes, so a decision-only entry point would be a
// second way to ask one question, distinguished by a saving too small to
// measure.
Authorize(ctx context.Context, tenant string, subject string, object string, action string) (Decision, error)
// RemoveRole removes role from tenant, including its permission policies and
// subject assignments. Callers should use this when deleting a role record so
// authorization state does not retain stale grants.
RemoveRole(ctx context.Context, tenant string, role string) error
// SetRolePermissions replaces the entire permission set held by role inside
// tenant with permissions, leaving the role's subject assignments untouched.
//
// It replaces rather than adds on purpose: the argument is the whole truth,
// so an entry the caller drops stops allowing requests, and passing an empty
// set revokes everything. A grant-only API would leave a removed entry
// allowing requests forever, with nothing left in the source to show it.
//
// Implementations must apply the whole set as one step. Revoking and then
// granting back one permission at a time exposes the role's members to an
// empty or partial set while the replacement is in flight, which denies
// requests the role is entitled to.
//
// It is the only way to write a role's permissions, which is why it takes
// the whole set: an interface offering a single grant beside it would let a
// caller build one up a row at a time and never learn that the entry it
// dropped is still allowing requests.
SetRolePermissions(ctx context.Context, tenant string, role string, permissions []Permission) error
// SetPermissionsForAuthenticated replaces the entire set of permissions every
// authenticated subject holds. The grant is bound to neither a tenant nor a
// role, so it reaches subjects that hold no role at all, in every tenant.
//
// It is SetRolePermissions for the implicit role every authenticated subject
// carries, and shares its contract: the argument is the whole truth, an empty
// set revokes everything, and the whole set is applied as one step.
//
// Reserve it for objects that answer only about the caller and already narrow
// their result to what the caller may see; anything else granted this way
// becomes reachable by every subject that can log in. Unauthenticated requests
// are unaffected, because authorization runs only after authentication.
SetPermissionsForAuthenticated(ctx context.Context, permissions []Permission) error
// AssignRole assigns subject to role inside tenant.
// This creates tenant membership for subject and makes the role's
// tenant-scoped permissions available to that subject.
AssignRole(ctx context.Context, tenant string, subject string, role string) error
// UnassignRole removes subject's assignment to role inside tenant.
// Other roles held by the same subject in the same tenant are left unchanged.
UnassignRole(ctx context.Context, tenant string, subject string, role string) error
// RolesForSubject returns the roles subject holds inside tenant.
//
// It answers both questions the pair it replaced answered separately:
// membership is a non-empty result, and holding one particular role is that
// role being among them. Neither deserved an entry point of its own, and
// keeping the general one leaves this and SubjectsInTenant as the two
// directions of a single relation.
RolesForSubject(ctx context.Context, tenant string, subject string) ([]string, error)
// SubjectsInTenant returns subjects with at least one role assignment in
// tenant. It checks membership, not whether any specific route is authorized.
SubjectsInTenant(ctx context.Context, tenant string) ([]string, error)
// AssignSystemRole assigns subject to a system-level role outside any tenant.
// System roles are intended for cross-tenant framework privileges and should
// not be used for ordinary tenant-local authorization.
AssignSystemRole(ctx context.Context, subject string, role string) error
// UnassignSystemRole removes subject's assignment to a system-level role.
UnassignSystemRole(ctx context.Context, subject string, role string) error
// HasSystemRole reports whether subject holds a system-level role.
// This check is separate from Authorize because system roles are not scoped to
// tenant route policies.
HasSystemRole(ctx context.Context, subject string, role string) (bool, error)
// RemoveSubject removes every role assignment held by subject, both
// tenant-scoped and system-level, across all tenants. Use this when a
// subject is deleted or deactivated so no orphaned role bindings remain.
RemoveSubject(ctx context.Context, subject string) error
// ReloadPolicies discards the authorization state the process holds in
// memory and rebuilds it from storage.
//
// Implementations answer from memory and keep it in step as they write, so
// the two agree as long as this process is the only writer. They stop
// agreeing when the stored rules change behind its back: another replica
// writing them, an operator repairing them by hand, a restore. Nothing
// detects that on its own, so this is the lever that puts a process back
// onto the stored state without restarting it.
//
// It reads every rule and is not part of the write path, which maintains
// memory itself. Reserve it for recovery and for the moment a change is
// known to have happened elsewhere.
ReloadPolicies(ctx context.Context) error
}
RBAC provides tenant-scoped role, permission, and subject assignment operations. A process holding no policy set — RBAC disabled, or not initialized — answers reads as the deployment they describe, denying every request and reporting no roles, and refuses every write rather than reporting a change it did not make.
RBAC Model:
- Tenant: Authorization domain for roles, permissions, and assignments
- Subject: Users or entities that need access
- Role: Named collection of permissions
- Object: Protected resources or endpoints
- Action: Operations on resources
type Request ¶
type Request any
Request and Response are the framework-facing types of one action's request and response payloads. They constrain the REQ and RSP type parameters of Service and Module; the concrete types are declared per action by the model layer.
type Response ¶
type Response any
Request and Response are the framework-facing types of one action's request and response payloads. They constrain the REQ and RSP type parameters of Service and Module; the concrete types are declared per action by the model layer.
type SQLStatement ¶
type SQLStatement struct {
// Query is the parameterized SQL with placeholders.
Query string
// Args contains the values bound to Query.
Args []any
// RenderedSQL is dialect-rendered SQL for logging, inspection, and manual debugging.
RenderedSQL string
}
SQLStatement contains a generated SQL statement in executable and rendered forms.
type Service ¶
type Service[M Model, REQ Request, RSP Response] interface { Create(*ServiceContext, REQ) (RSP, error) Delete(*ServiceContext, REQ) (RSP, error) Update(*ServiceContext, REQ) (RSP, error) Patch(*ServiceContext, REQ) (RSP, error) List(*ServiceContext, REQ) (RSP, error) Get(*ServiceContext, REQ) (RSP, error) CreateMany(*ServiceContext, REQ) (RSP, error) DeleteMany(*ServiceContext, REQ) (RSP, error) UpdateMany(*ServiceContext, REQ) (RSP, error) PatchMany(*ServiceContext, REQ) (RSP, error) CreateBefore(*ServiceContext, M) error CreateAfter(*ServiceContext, M) error DeleteBefore(*ServiceContext, M) error DeleteAfter(*ServiceContext, M) error UpdateBefore(*ServiceContext, M) error UpdateAfter(*ServiceContext, M) error PatchBefore(*ServiceContext, M) error PatchAfter(*ServiceContext, M) error ListBefore(*ServiceContext, *[]M) error ListAfter(*ServiceContext, *[]M) error GetBefore(*ServiceContext, M) error GetAfter(*ServiceContext, M) error CreateManyBefore(*ServiceContext, ...M) error CreateManyAfter(*ServiceContext, ...M) error DeleteManyBefore(*ServiceContext, ...M) error DeleteManyAfter(*ServiceContext, ...M) error UpdateManyBefore(*ServiceContext, ...M) error UpdateManyAfter(*ServiceContext, ...M) error PatchManyBefore(*ServiceContext, ...M) error PatchManyAfter(*ServiceContext, ...M) error Import(*ServiceContext, io.Reader) ([]M, error) Export(*ServiceContext, ...M) ([]byte, error) // SSE streams Server-Sent Events for the route: the implementation opens // the stream via ServiceContext.SSE and blocks until it is over. Query // parameters are read from ServiceContext.Query(). The action never binds // Payload or Result types, so the method carries no REQ or RSP. SSE(*ServiceContext) error // Filter lets a service rewrite the query condition before the // controller-side listing runs (List and Export). The model carries the // URL-decoded equality condition and the options carry the parsed operator // filters; the typical use is row-level data scoping: append typed filters // (e.g. Cols.GroupID.In(...)) to options.Filters or narrow the model // condition, then return both. Returning an error aborts the request — the // correct behavior when loading the caller's data scope fails. The // controller calls Filter once and shares the result between List and // Count, so both always see the same condition set. Filter(*ServiceContext, M, QueryOptions) (M, QueryOptions, error) Logger }
Service defines the controller-facing business operation contract for a model. Generated controllers call these methods for CRUD, batch CRUD, lifecycle hooks, import/export, filtering, and logging.
Type Parameters:
- M: Model type that implements Model interface
- REQ: Request type for the current action or resource operation
- RSP: Response type for the current action or resource operation
Custom actions should use action-specific REQ/RSP types instead of reusing types from other endpoints, even when the fields are identical.
Nil-safety contract: when invoked by the generated controllers, ctx is never nil and req is never a nil pointer — the controller constructs a fresh *ServiceContext per call and instantiates REQ via reflection before binding, so implementations do not need defensive nil checks on ctx or req.
Non-nil does not mean populated: List/Get never bind a request body, and Create/Update tolerate an empty body, so req may point to a zero-value struct. Validate required business fields instead of checking for nil.
The contract only covers framework-invoked calls. Code that calls a service method directly (tests, jobs, or code bypassing the controller layer) must supply non-nil arguments itself.
type ServiceContext ¶
type ServiceContext struct {
// contains filtered or unexported fields
}
ServiceContext is the per-request context the framework hands to every service method. It implements context.Context by delegating to the request context, exposes request metadata (route, params, user identity, trace), and carries the response helpers a service needs without touching Gin directly.
func NewServiceContext ¶
NewServiceContext builds a ServiceContext from the Gin request, capturing request details, phase, and user metadata.
A non-nil ctx overrides the base context, which is how span tracing is propagated; when ctx is nil, the request context is used when available.
NewServiceContext always returns a non-nil *ServiceContext, even when both c and ctx are nil. ServiceContext methods are also nil-receiver safe and return zero values on a nil receiver, so callers never need defensive nil checks around the returned context.
func (*ServiceContext) ClientIP ¶
func (sc *ServiceContext) ClientIP() string
func (*ServiceContext) Data ¶
func (sc *ServiceContext) Data(code int, contentType string, data []byte)
func (*ServiceContext) Done ¶
func (sc *ServiceContext) Done() <-chan struct{}
func (*ServiceContext) Err ¶
func (sc *ServiceContext) Err() error
func (*ServiceContext) FormFile ¶
func (sc *ServiceContext) FormFile(name string) (*multipart.FileHeader, error)
func (*ServiceContext) Host ¶
func (sc *ServiceContext) Host() string
func (*ServiceContext) IsHTTPS ¶
func (sc *ServiceContext) IsHTTPS() bool
func (*ServiceContext) Method ¶
func (sc *ServiceContext) Method() string
func (*ServiceContext) Param ¶
func (sc *ServiceContext) Param(key string) string
func (*ServiceContext) Path ¶
func (sc *ServiceContext) Path() string
func (*ServiceContext) Phase ¶
func (sc *ServiceContext) Phase() consts.Phase
func (*ServiceContext) PostForm ¶
func (sc *ServiceContext) PostForm(key string) string
func (*ServiceContext) Query ¶
func (sc *ServiceContext) Query() url.Values
func (*ServiceContext) RequiresAuth ¶
func (sc *ServiceContext) RequiresAuth() bool
RequiresAuth returns whether the current API requires authentication.
func (*ServiceContext) Route ¶
func (sc *ServiceContext) Route() string
func (*ServiceContext) SSE ¶
SSE turns the response into a Server-Sent Events stream and runs fn with the live connection.
The framework owns the connection lifecycle: it clears the server's per-request deadlines so the stream outlives the global WriteTimeout, writes and flushes the SSE response headers, sends keep-alive comment frames until fn returns, and invalidates the connection afterwards. fn blocks until the stream is over; a callback that waits for events must select on conn.Context().Done() to notice the client disconnecting.
The error is fn's own error, or the setup failure that prevented streaming (reported before anything was written, so it still surfaces as a regular error response).
Example:
return nil, ctx.SSE(func(conn *sse.Conn) error {
for {
select {
case <-conn.Context().Done():
return nil
case event := <-events:
if err := conn.Send(event); err != nil {
return err
}
}
}
})
Example ¶
ExampleServiceContext_SSE demonstrates streaming a fixed number of events.
package main
import (
"github.com/hydroan/gst/sse"
"github.com/hydroan/gst/types"
)
func main() {
var sc *types.ServiceContext // acquired from a service method in real code
_ = sc.SSE(func(conn *sse.Conn) error {
for i := 1; i <= 3; i++ {
if err := conn.Send(sse.Event{Event: "message", Data: i}); err != nil {
return err
}
}
return nil
})
}
Output:
Example (WaitForEvents) ¶
ExampleServiceContext_SSE_waitForEvents demonstrates the event-driven shape: the callback blocks on an event source and stops when the client is gone.
package main
import (
"github.com/hydroan/gst/sse"
"github.com/hydroan/gst/types"
)
func main() {
var sc *types.ServiceContext // acquired from a service method in real code
events := make(chan sse.Event)
_ = sc.SSE(func(conn *sse.Conn) error {
for {
select {
case <-conn.Context().Done():
return nil
case event := <-events:
if err := conn.Send(event); err != nil {
return err
}
}
}
})
}
Output:
func (*ServiceContext) SessionID ¶
func (sc *ServiceContext) SessionID() string
func (*ServiceContext) SetCookie ¶
func (sc *ServiceContext) SetCookie(cookie *http.Cookie)
func (*ServiceContext) TenantID ¶
func (sc *ServiceContext) TenantID() string
func (*ServiceContext) TraceID ¶
func (sc *ServiceContext) TraceID() string
func (*ServiceContext) UserAgent ¶
func (sc *ServiceContext) UserAgent() string
func (*ServiceContext) UserID ¶
func (sc *ServiceContext) UserID() string
func (*ServiceContext) Username ¶
func (sc *ServiceContext) Username() string
func (*ServiceContext) Value ¶
func (sc *ServiceContext) Value(key any) any
type StandardLogger ¶
type StandardLogger interface {
Debug(args ...any)
Info(args ...any)
Warn(args ...any)
Error(args ...any)
Fatal(args ...any)
Debugf(format string, args ...any)
Infof(format string, args ...any)
Warnf(format string, args ...any)
Errorf(format string, args ...any)
Fatalf(format string, args ...any)
}
StandardLogger provides plain and printf-style leveled logging methods. Fatal and Fatalf follow the underlying logger's fatal behavior and should terminate the process after writing the log entry.
type StructuredLogger ¶
type StructuredLogger interface {
Debugw(msg string, keysAndValues ...any)
Infow(msg string, keysAndValues ...any)
Warnw(msg string, keysAndValues ...any)
Errorw(msg string, keysAndValues ...any)
Fatalw(msg string, keysAndValues ...any)
}
StructuredLogger provides sugared structured logging with alternating key/value fields. Methods with suffix "w" mean "with fields".
type Subquery ¶
type Subquery struct {
// Model is an allocated instance of the related model. It carries the
// child table name and its soft-delete scope, so a subquery hides the same
// rows a List on that model hides.
Model Model
// Filters narrow the related rows. They must include a FilterCorrelate,
// directly or inside a group: without one the subquery would be a cross
// join, so it fails closed instead.
Filters []Filter
// Negate turns the condition into NOT EXISTS.
Negate bool
}
Subquery is the correlated EXISTS subquery carried by FilterOpExists. It names the related model and the predicates narrowing its rows, at least one of which must be a FilterCorrelate tying them to the enclosing query.
A semi join is used rather than a real join on purpose: EXISTS matches a row at most once, so an aggregate over the outer table keeps counting each row once. A join to a one-to-many child multiplies the outer rows instead, and a SUM over that silently doubles.
type TimeBucket ¶
type TimeBucket string
TimeBucket is the truncation granularity of a time group key. Bucketing is the one place where the same intent needs a different expression per dialect, so the constant travels through the builder and the database layer renders it; callers never see a format string.
const ( // TimeBucketNone groups by the raw column value. TimeBucketNone TimeBucket = "" TimeBucketHour TimeBucket = "hour" TimeBucketDay TimeBucket = "day" TimeBucketMonth TimeBucket = "month" )
func (TimeBucket) Valid ¶
func (b TimeBucket) Valid() bool
Valid reports whether the bucket is one this package defines. An unknown bucket would otherwise fall through to the day granularity and silently report the wrong period.
type TimeColumn ¶
TimeColumn is the reference generated for a time.Time column. It embeds Column and adds time bucketing, which is only meaningful over a time value and produces garbage rather than an error on some dialects when it is not.
func NewTimeColumn ¶
func NewTimeColumn(name string) TimeColumn
NewTimeColumn returns the time reference to the named database column, carrying the bucketing group keys on top of everything Column has.
func (TimeColumn) ByDay ¶
func (c TimeColumn) ByDay() AggregateTerm
func (TimeColumn) ByHour ¶
func (c TimeColumn) ByHour() AggregateTerm
ByHour, ByDay and ByMonth make this column a group key truncated to the bucket, which is what a trend report groups by. The truncation expression differs per dialect and is rendered by the database layer, so callers never deal with a format string.
func (TimeColumn) ByMonth ¶
func (c TimeColumn) ByMonth() AggregateTerm
type ZapLogger ¶
type ZapLogger interface {
Debugz(msg string, fields ...zap.Field)
Infoz(msg string, fields ...zap.Field)
Warnz(msg string, fields ...zap.Field)
Errorz(msg string, fields ...zap.Field)
Fatalz(msg string, fields ...zap.Field)
}
ZapLogger provides structured logging with typed zap.Field values. Methods with suffix "z" are the low-allocation typed-field variants.