Documentation
¶
Overview ¶
Package store provides a generic CRUD store backed by GORM.
Index ¶
- Variables
- func MapError(err error) *apierr.Error
- func SetDefaultAdminRoles(roles ...string)deprecated
- func WithVersion(v int) versionOpt
- type Changes
- type CursorPage
- type DeleteOption
- type DuplicateEntryError
- type DuplicateError
- type FieldChanges
- type Locator
- type NotFoundError
- type Page
- type QueryOption
- type ReadWriter
- type Reader
- type ScopeFunc
- type Store
- func (s *Store[T]) BatchCreate(ctx context.Context, objs []*T) error
- func (s *Store[T]) Create(ctx context.Context, obj *T) error
- func (s *Store[T]) DB() *gorm.DB
- func (s *Store[T]) Delete(ctx context.Context, by Locator, opts ...DeleteOption) error
- func (s *Store[T]) Exists(ctx context.Context, by Locator) (bool, error)
- func (s *Store[T]) Get(ctx context.Context, by Locator, opts ...QueryOption) (*T, error)
- func (s *Store[T]) List(ctx context.Context, opts ...where.Option) (*Page[T], error)
- func (s *Store[T]) ListByIDs(ctx context.Context, ids []uint) ([]T, error)
- func (s *Store[T]) ListFromQuery(ctx context.Context, query url.Values) ([]T, int64, error)
- func (s *Store[T]) ListQ(ctx context.Context, qopts []QueryOption, opts ...where.Option) (*Page[T], error)
- func (s *Store[T]) ListWithCursor(ctx context.Context, cursorField string, direction where.CursorDirection, ...) (*CursorPage[T], error)
- func (s *Store[T]) ScopedDB(ctx context.Context) (*gorm.DB, error)
- func (s *Store[T]) Tx(ctx context.Context, fn func(tx *Store[T]) error) error
- func (s *Store[T]) Update(ctx context.Context, by Locator, changes Changes, opts ...UpdateOption) error
- func (s *Store[T]) Upsert(ctx context.Context, obj *T, conflictColumns []string, updateColumns ...string) error
- func (s *Store[T]) WithTx(tx *gorm.DB) *Store[T]
- type StoreOption
- func WithAfterCreate[T db.Modeler](fn func(ctx context.Context, obj *T)) StoreOption
- func WithAfterDelete(fn func(ctx context.Context, loc Locator)) StoreOption
- func WithAfterUpdate(fn func(ctx context.Context, loc Locator, changes Changes)) StoreOption
- func WithAllQueryFields(exclude ...string) StoreOption
- func WithAllUpdateFields(exclude ...string) StoreOption
- func WithAsyncAfterCreate[T db.Modeler](pool Submitter, fn func(ctx context.Context, obj *T)) StoreOption
- func WithAsyncAfterDelete(pool Submitter, fn func(ctx context.Context, loc Locator)) StoreOption
- func WithAsyncAfterUpdate(pool Submitter, fn func(ctx context.Context, loc Locator, changes Changes)) StoreOption
- func WithBeforeCreate[T db.Modeler](fn func(ctx context.Context, obj *T) error) StoreOption
- func WithBeforeDelete(fn func(ctx context.Context, loc Locator) error) StoreOption
- func WithBeforeUpdate(fn func(ctx context.Context, loc Locator, changes Changes) error) StoreOption
- func WithColumnAlias(field, column string) StoreOption
- func WithDefaultPageSize(size int) StoreOption
- func WithMaxPageSize(n int) StoreOption
- func WithQueryFields(fields ...string) StoreOption
- func WithRequirePrincipal() StoreOption
- func WithScope(scope ScopeFunc) StoreOption
- func WithStrict() StoreOption
- func WithUpdateFields(fields ...string) StoreOption
- func WithoutOwnerScope() StoreOption
- type Submitter
- type UpdateOption
- type VersionConflictError
- type Writer
Constants ¶
This section is empty.
Variables ¶
var ( ErrNotFound = errors.New("store: record not found") ErrStaleVersion = errors.New("store: version conflict, row was modified by another request") ErrMissingColumns = errors.New("store: update called without columns") ErrMissingConditions = errors.New("store: operation called without conditions") // ErrDegenerateConditions means the locator's filter is present but // collapses to match-nothing (e.g. WithFilterIn over an empty slice, // WithFilter with a nil value). Distinguishing this from "no filter" // lets Update/Delete callers surface a precise client-input error // rather than silently succeeding with zero rows affected. ErrDegenerateConditions = errors.New("store: filter matches nothing") ErrDuplicate = errors.New("store: duplicate entry") // ErrUnknownUpdateField indicates the field name is not in the update whitelist. // This is a programming error (code passes a wrong field constant), not client input. ErrUnknownUpdateField = errors.New("store: unknown update field") // ErrUpdateFieldsNotConfigured indicates WithUpdateFields was not called. // This is a programming error (Store misconfigured), not client input. ErrUpdateFieldsNotConfigured = errors.New("store: update fields not configured") // ErrUpsertScoped indicates Upsert was called on a Store that has // scopes registered. SQL INSERT ... ON CONFLICT DO UPDATE does not // honour WHERE-based scope conditions on the conflict update path, // so a conflict on a globally unique column could silently bypass // tenant isolation or other scope invariants. Use Create + Update, // or s.DB() as an escape hatch if you understand the implications. ErrUpsertScoped = errors.New("store: upsert is not safe with scoped stores (scopes are ignored on conflict update); use separate Create + Update") )
Sentinel errors — business code uses these without importing GORM. errors.Is works with both the sentinels and the structured *XxxError types (which implement Is(target) bool).
Functions ¶
func MapError ¶
MapError maps store sentinel errors to *apierr.Error. Returns nil for unrecognized errors.
Only client-visible errors are mapped. Programming errors (ErrMissingConditions, ErrMissingColumns) are intentionally excluded — they are server-side bugs and should surface as 500, not mislead the client with a 400.
Usage: register per-App via chok.WithErrorMapper:
chok.New("app", chok.WithErrorMapper(store.MapError), ...)
func SetDefaultAdminRoles
deprecated
func SetDefaultAdminRoles(roles ...string)
SetDefaultAdminRoles sets the global admin role names used by auto-detected OwnerScope. Call once at startup before creating any Store.
Deprecated: Global admin roles are shared across all Store instances. Prefer per-Store admin roles via OwnerScope("admin", "superadmin") passed to WithScope at Store construction. This function will be removed in a future release.
func WithVersion ¶
func WithVersion(v int) versionOpt
WithVersion enables optimistic locking by asserting the row's current version column equals v. The returned option satisfies both UpdateOption and DeleteOption.
Use WithVersion when Changes is Set(map) — the map carries no version field, so the lock must be supplied separately. For Fields(&obj) the version is extracted from obj.Version automatically; WithVersion there overrides the implicit value (rare).
A version <= 0 is treated as "no lock" (idempotent semantics).
Types ¶
type Changes ¶
type Changes interface {
// contains filtered or unexported methods
}
Changes describes what to update on matched rows. It is the "what" axis of the CRUD matrix, orthogonal to Locator ("who") and UpdateOption ("how").
Two constructors cover all common cases:
- Set(map) — explicit map of public field names to values. No implicit optimistic lock; use WithVersion to enable.
- Fields(obj, fields...) — struct-backed update. Automatically extracts the optimistic-lock version from obj.Version unless .NoLock() is called. Omitting fields updates every field in the Store's update whitelist.
func Set ¶
Set returns a Changes that applies literal map values to the matched row. Keys are public field names resolved via the Store's update whitelist; unknown keys return ErrUnknownUpdateField. Empty map returns ErrMissingColumns.
Set does NOT enable optimistic locking on its own — pairing with WithVersion is explicit:
store.Update(ctx, store.RID(x), store.Set(cols), store.WithVersion(v))
type CursorPage ¶
type CursorPage[T any] struct { Items []T `json:"items"` NextCursor string `json:"next_cursor,omitempty"` }
CursorPage is the result of a cursor-based paginated query. NextCursor is the value to pass as the cursor argument for the next page; empty string means no more pages. Items are guaranteed non-nil.
type DeleteOption ¶
type DeleteOption interface {
// contains filtered or unexported methods
}
DeleteOption tunes Delete behaviour.
type DuplicateEntryError ¶
type DuplicateEntryError struct {
Detail string // driver-specific constraint/message
}
DuplicateEntryError carries the raw database error detail so callers can report which constraint was violated.
func (*DuplicateEntryError) Error ¶
func (e *DuplicateEntryError) Error() string
func (*DuplicateEntryError) Is ¶
func (e *DuplicateEntryError) Is(target error) bool
Is makes errors.Is(err, ErrDuplicate) return true.
type DuplicateError ¶
type DuplicateError interface {
IsDuplicate() bool
}
DuplicateError is an optional interface that database drivers or error wrappers can implement for reliable duplicate-key detection without string matching. When the error chain contains a DuplicateError implementation, isDuplicateError trusts it over heuristics.
type FieldChanges ¶
type FieldChanges struct {
// contains filtered or unexported fields
}
FieldChanges is the concrete return of Fields so that .NoLock() remains available. It still satisfies the Changes interface.
func Fields ¶
func Fields(obj any, fields ...string) *FieldChanges
Fields returns a FieldChanges that updates selected fields of obj.
When fields is empty, every column declared via WithUpdateFields is written (whole-whitelist update). Zero values in obj ARE persisted — the Store internally uses Select(cols...).Updates(obj) to bypass GORM's default "skip zero values" behaviour, which would otherwise silently drop clears (e.g. setting a bio back to "").
Fields auto-detects optimistic locking: if obj embeds db.Model (directly or via db.SoftDeleteModel), the current obj.Version is used as the WHERE version guard and incremented on success. Call .NoLock() to opt out for admin overrides or concurrent-safe fields.
func (*FieldChanges) NoLock ¶
func (f *FieldChanges) NoLock() *FieldChanges
NoLock disables the automatic optimistic-lock behaviour of Fields. Use this for admin-level overrides ("force save, ignore version") or when the caller already decided a first-write-wins policy is acceptable.
type Locator ¶
type Locator interface {
// contains filtered or unexported methods
}
Locator identifies which record(s) a Get/Update/Delete operation targets. It is the "who" axis of the CRUD matrix, orthogonal to Changes ("what") and UpdateOption/DeleteOption ("how").
The three built-in locators cover all common cases:
- RID(rid) — external contract, safe to expose to clients
- ID(id) — internal numeric PK, for cross-table joins and batch work
- Where(opts...) — arbitrary conditions via the where DSL (whitelist-enforced)
Locator.apply runs after scopes (OwnerScope, etc.) are applied, so the final WHERE clause is the intersection of scopes and locator conditions.
func ID ¶
ID returns a Locator matching records by their internal numeric primary key. Intended for server-side cross-table lookups where foreign keys are numeric. Do not expose the numeric ID to external clients.
type NotFoundError ¶
type NotFoundError struct {
Locator string // e.g. "rid:usr_abc", "id", "where"
RID string // populated when by=store.RID(...); see HasRID
IDValue uint // populated when by=store.ID(...); see HasID
HasRID bool
HasID bool
}
NotFoundError carries context about what was not found.
Locator is a redacted representation safe for error.Error() output (numeric IDs are printed as "id" without the value to avoid leaking internal primary keys to logs that might propagate to clients). For server-side diagnostics, RID and IDValue are populated when the locator was store.RID(...) / store.ID(...) respectively — callers can use errors.As and type-read these fields without parsing Locator.
HasRID / HasID flags distinguish "locator was RID/ID" from zero-value ambiguity: store.RID("") or store.ID(0) are unusual but the error path should still signal which kind of lookup was attempted.
func (*NotFoundError) Error ¶
func (e *NotFoundError) Error() string
func (*NotFoundError) Is ¶
func (e *NotFoundError) Is(target error) bool
Is makes errors.Is(err, ErrNotFound) return true.
type Page ¶
Page is the result of a paginated list query. Items is guaranteed non-nil. Total is the total number of matching records when where.WithCount() is included in the query options; zero when count is not requested.
type QueryOption ¶
type QueryOption func(*queryConfig)
QueryOption configures a Get or List query with additional clauses that sit outside the where DSL (preloads, joins, etc.).
func WithOnlyTrashed ¶
func WithOnlyTrashed() QueryOption
WithOnlyTrashed returns ONLY soft-deleted records. Useful for "trash" views or data recovery workflows. Like WithTrashed, all scopes still apply.
func WithPreload ¶
func WithPreload(relation string) QueryOption
WithPreload eagerly loads a named association using GORM's Preload. The relation name must match the Go struct field name (e.g. "Author", "Tags"). Multiple WithPreload options can be combined.
Scope propagation: the store's scopes (OwnerScope + any custom scope) are re-applied to the association query, so preloading never leaks rows owned by other principals. This is safer than s.DB().Preload() which would bypass scopes entirely. Custom scopes whose predicates don't make sense on the associated table must detect this via ctx and return q unchanged to opt out; the scope function itself is the single point of enforcement.
post, err := s.Get(ctx, store.RID(rid), store.WithPreload("Author"))
page, err := s.List(ctx, where.WithCount(), store.WithPreload("Tags"))
func WithTrashed ¶
func WithTrashed() QueryOption
WithTrashed includes soft-deleted records in the query result. GORM normally adds `WHERE deleted_at IS NULL` automatically for models with a DeletedAt field; this option removes that filter. Scopes (OwnerScope etc.) are still applied — soft-deleted records are visible but not unprotected.
Use this in admin/audit views that need to see deleted records.
type ReadWriter ¶
ReadWriter combines Reader and Writer into a single interface covering standard CRUD operations without escape hatches (DB/ScopedDB/Tx).
type Reader ¶
type Reader[T db.Modeler] interface { Get(ctx context.Context, by Locator, opts ...QueryOption) (*T, error) List(ctx context.Context, opts ...where.Option) (*Page[T], error) ListFromQuery(ctx context.Context, query url.Values) ([]T, int64, error) ListByIDs(ctx context.Context, ids []uint) ([]T, error) Exists(ctx context.Context, by Locator) (bool, error) }
Reader is a read-only view of a Store. Business code that only queries data should depend on this interface rather than the full *Store[T].
type ScopeFunc ¶
ScopeFunc applies context-derived query conditions directly to *gorm.DB. It bypasses the WithQueryFields whitelist (scope is an internal security constraint, not a client-facing query field). Returns error to enforce fail-closed: unauthenticated contexts must return an error rather than silently skipping the filter. If the error should map to a specific HTTP status (e.g. 401), return *apierr.Error.
func OwnerScope ¶
OwnerScope returns a ScopeFunc that restricts queries to the current principal's own records (WHERE owner_id = subject). Principals holding any of adminRoles bypass the filter and see all records.
Unauthenticated requests fail closed with ErrUnauthenticated.
Usage:
store.New[Product](gdb, logger,
store.WithScope(store.OwnerScope("admin")),
)
type Store ¶
Store is a generic CRUD store for models embedding db.Model.
func New ¶
New creates a Store. T must be a struct type (not a pointer). Panics if T is a pointer type or has an invalid RIDPrefix.
func (*Store[T]) BatchCreate ¶
BatchCreate inserts multiple records in a single transaction. Empty slice returns nil (no-op). Single failure rolls back the entire batch. If the model embeds db.Owned, OwnerID is auto-filled from the principal. Before-hooks run for each object before the transaction starts. Returns ErrDuplicate on unique constraint violation.
func (*Store[T]) Create ¶
Create inserts a new record. If the model embeds db.Owned and OwnerID is empty, it is auto-filled from the authenticated principal's Subject. Returns ErrDuplicate on unique constraint violation.
func (*Store[T]) DB ¶
DB returns the underlying *gorm.DB as an escape hatch for complex queries. The returned handle has NO scopes applied — use ScopedDB when you need OwnerScope / custom scopes to remain in force.
func (*Store[T]) Delete ¶
Delete removes the record(s) matched by the locator. Soft-delete models get deleted_at + a fresh delete_token; regular models are physically deleted.
Without WithVersion, Delete is idempotent — zero matches returns nil. With WithVersion, a zero-match row that exists returns ErrStaleVersion; a truly absent row returns ErrNotFound.
func (*Store[T]) Exists ¶
Exists checks whether any record matches the locator under the Store's scopes. More efficient than Get when you only need presence, not the data.
func (*Store[T]) Get ¶
Get retrieves a single record matched by the locator. Returns ErrNotFound if no record matches. Scopes (OwnerScope, custom) are applied before the locator's WHERE. Optional QueryOption (e.g. WithPreload) can be appended.
Examples:
store.Get(ctx, store.RID("usr_abc"))
store.Get(ctx, store.ID(42), store.WithPreload("Author"))
func (*Store[T]) List ¶
List retrieves records. Zero matches returns a Page with empty Items slice. Total is populated only when where.WithCount() is included; zero otherwise.
func (*Store[T]) ListByIDs ¶
ListByIDs retrieves records matching the given internal numeric IDs. Empty ids returns an empty slice. Order is not guaranteed. Intended for server-side batch joins across tables.
func (*Store[T]) ListFromQuery ¶
ListFromQuery parses URL query parameters and returns a paginated list. Supported query params: page, size, order (field:asc|desc), and any field declared via WithQueryFields as an equality filter. Fixed filters should be applied via WithScope at Store construction time.
Unknown query parameters are silently ignored unless the Store was constructed with WithStrict, in which case they return apierr.ErrInvalidArgument so clients get immediate feedback instead of silent "my filter didn't apply, why?" debugging.
func (*Store[T]) ListQ ¶
func (s *Store[T]) ListQ(ctx context.Context, qopts []QueryOption, opts ...where.Option) (*Page[T], error)
ListQ is like List but additionally accepts QueryOptions (e.g. WithTrashed, WithPreload). QueryOptions are applied after scopes, so security constraints remain in effect.
page, err := s.ListQ(ctx, []store.QueryOption{store.WithTrashed()}, where.WithCount())
func (*Store[T]) ListWithCursor ¶
func (s *Store[T]) ListWithCursor(ctx context.Context, cursorField string, direction where.CursorDirection, cursorValue any, size int, opts ...where.Option) (*CursorPage[T], error)
ListWithCursor performs keyset (cursor-based) pagination. Unlike offset pagination, cursor pagination is O(1) regardless of page depth, making it suitable for mobile infinite-scroll and public APIs.
cursorField is validated against the query whitelist. cursorValue is the last-seen value from the previous page (empty string or nil for the first page). size is the max items per page. Additional opts can add filters.
Example:
page, err := s.ListWithCursor(ctx, "id", where.CursorAfter, lastID, 20)
func (*Store[T]) ScopedDB ¶
ScopedDB returns a *gorm.DB with WithContext(ctx) applied and all registered scopes (including auto-detected OwnerScope) evaluated.
Use this when writing custom queries on an extended Store wrapper — e.g.:
func (s *BookshelfStore) FindByBookID(ctx context.Context, id uint) (*Item, error) {
q, err := s.ScopedDB(ctx)
if err != nil { return nil, err }
var item Item
return &item, q.Where("book_id = ?", id).First(&item).Error
}
Scope errors (e.g. ErrUnauthenticated from OwnerScope with no principal in ctx) are returned to the caller; do NOT fall back to s.DB() on error — that would leak cross-tenant data.
func (*Store[T]) Tx ¶
Tx runs fn inside a transaction scoped to this Store. fn receives a Store bound to the transaction's DB handle.
If a context-scoped transaction is already active (via db.RunInTx), Tx reuses it instead of opening a nested transaction. The txCtx is threaded through db.RunInTx so that any other Store / helper called with txCtx inside fn will also join the same transaction — matching the contract of db.RunInTx.
func (*Store[T]) Update ¶
func (s *Store[T]) Update(ctx context.Context, by Locator, changes Changes, opts ...UpdateOption) error
Update modifies the record(s) matched by the locator using the described changes. Optimistic locking is automatic when Changes is Fields(&obj) and obj embeds db.Model; use WithVersion for explicit locking with Set(map), or .NoLock() on Fields to skip the lock.
Returns:
- ErrNotFound when no row matches the locator
- ErrStaleVersion when the lock version is stale (row exists but version mismatch)
- ErrUnknownUpdateField / ErrMissingColumns for invalid Changes
Zero values in Fields(&obj) ARE persisted — the Store uses Select() to bypass GORM's default "skip zero values" behaviour.
func (*Store[T]) Upsert ¶
func (s *Store[T]) Upsert(ctx context.Context, obj *T, conflictColumns []string, updateColumns ...string) error
Upsert inserts obj or updates it on conflict. conflictColumns are the unique constraint columns (resolved via the query field whitelist) that trigger the "update" path. updateColumns are the columns to update on conflict (resolved via the update field whitelist). When updateColumns is empty, all update-whitelisted columns are updated.
Before-create hooks fire before the SQL. Owner auto-fill applies.
Upsert is forbidden on scoped Stores AND on Stores whose model embeds db.Owned (even when OwnerScope is disabled via WithoutOwnerScope). SQL ON CONFLICT UPDATE does not apply the owner_id WHERE filter to the update path, so an attacker providing a conflicting key that belongs to another user could mutate the victim's row. Use Create + detect ErrDuplicate + Update as an explicit alternative.
type StoreOption ¶
type StoreOption func(*storeConfig)
StoreOption configures a Store.
func WithAfterCreate ¶
func WithAfterCreate[T db.Modeler](fn func(ctx context.Context, obj *T)) StoreOption
WithAfterCreate registers a fire-and-forget callback that runs after a successful Create. The row is already committed — the callback cannot affect the caller's result. Multiple callbacks run in registration order.
Typical uses: audit logging, cache invalidation, async event publishing.
func WithAfterDelete ¶
func WithAfterDelete(fn func(ctx context.Context, loc Locator)) StoreOption
WithAfterDelete registers a fire-and-forget callback that runs after a successful Delete. The row is already committed.
func WithAfterUpdate ¶
func WithAfterUpdate(fn func(ctx context.Context, loc Locator, changes Changes)) StoreOption
WithAfterUpdate registers a fire-and-forget callback that runs after a successful Update. The row is already committed.
func WithAllQueryFields ¶
func WithAllQueryFields(exclude ...string) StoreOption
WithAllQueryFields auto-discovers queryable fields from the model's JSON tags. Fields tagged json:"-" are excluded (internal fields like PasswordHash, OwnerID). Optional exclude list removes specific fields by JSON name:
store.WithAllQueryFields() // all public fields
store.WithAllQueryFields("content") // all except content
func WithAllUpdateFields ¶
func WithAllUpdateFields(exclude ...string) StoreOption
WithAllUpdateFields auto-discovers updatable fields from the model's JSON tags. Base model fields (id, version, created_at, updated_at) and json:"-" fields are excluded. Text/blob fields are NOT excluded (updating content is normal). Optional exclude list removes additional fields by JSON name.
In strict mode (WithStrict), calling this option signals explicit consent to auto-discover so construction does not panic. Without this option, strict mode requires WithUpdateFields.
func WithAsyncAfterCreate ¶
func WithAsyncAfterCreate[T db.Modeler](pool Submitter, fn func(ctx context.Context, obj *T)) StoreOption
WithAsyncAfterCreate wraps fn in a Pool.SubmitFunc call so the hook runs asynchronously in the Pool's worker goroutines instead of blocking the request. The pool must be initialized before the Store is used.
func WithAsyncAfterDelete ¶
func WithAsyncAfterDelete(pool Submitter, fn func(ctx context.Context, loc Locator)) StoreOption
WithAsyncAfterDelete wraps fn in a Pool.SubmitFunc call.
func WithAsyncAfterUpdate ¶
func WithAsyncAfterUpdate(pool Submitter, fn func(ctx context.Context, loc Locator, changes Changes)) StoreOption
WithAsyncAfterUpdate wraps fn in a Pool.SubmitFunc call.
func WithBeforeCreate ¶
WithBeforeCreate registers a callback that runs before a Create writes to the database. Returning an error aborts the Create — no row is written and the caller sees the hook's error. Multiple callbacks run in registration order; the first error short-circuits.
Typical uses: cross-field validation, value normalisation, permission checks that can't be expressed as scopes.
func WithBeforeDelete ¶
func WithBeforeDelete(fn func(ctx context.Context, loc Locator) error) StoreOption
WithBeforeDelete registers a callback that runs before a Delete writes to the database. Returning an error aborts the Delete.
func WithBeforeUpdate ¶
WithBeforeUpdate registers a callback that runs before an Update writes to the database. Returning an error aborts the Update.
func WithColumnAlias ¶
func WithColumnAlias(field, column string) StoreOption
WithColumnAlias maps a public field name to a different database column (e.g. "id" → "rid"). The field must be declared via WithQueryFields or WithUpdateFields; otherwise panics at Store construction.
func WithDefaultPageSize ¶
func WithDefaultPageSize(size int) StoreOption
WithDefaultPageSize sets the default page size for ListFromQuery when the client does not provide a "size" parameter. Default is 20.
func WithMaxPageSize ¶
func WithMaxPageSize(n int) StoreOption
WithMaxPageSize sets a hard cap on page size for List / ListFromQuery. Requests exceeding this limit are silently clamped. Zero disables the cap.
func WithQueryFields ¶
func WithQueryFields(fields ...string) StoreOption
WithQueryFields declares which fields are queryable/sortable. Column name defaults to the field name. Fields not declared here are rejected by WithFilter/WithOrder.
func WithRequirePrincipal ¶
func WithRequirePrincipal() StoreOption
WithRequirePrincipal makes Create / BatchCreate / Upsert reject contexts without an authenticated principal when the model embeds db.Owned. This is fail-closed behaviour — safer for HTTP paths where a missing Authn middleware would otherwise let a client set OwnerID freely.
Background jobs and tests that legitimately write Owned rows without a principal must either:
- Not enable this option on those stores, or
- Attach a system principal to ctx via auth.WithPrincipal before Create.
Non-Owned models are unaffected.
func WithScope ¶
func WithScope(scope ScopeFunc) StoreOption
WithScope registers a scope function applied to every read/update/delete query (not Create/BatchCreate). See ScopeFunc for semantics. Panics if scope is nil (configuration error caught at startup).
func WithStrict ¶
func WithStrict() StoreOption
WithStrict enables strict mode for production safety:
- Auto-discovered query/update fields are rejected at construction time unless the caller explicitly opts in via WithAllQueryFields / WithAllUpdateFields. Without either, strict construction panics (prevents accidental "all fields queryable" exposure).
- ListFromQuery rejects unknown query parameters with apierr.ErrInvalidArgument instead of silently dropping them.
Intended for production configs where the implicit "discover from JSON tags" behavior is too permissive.
func WithUpdateFields ¶
func WithUpdateFields(fields ...string) StoreOption
WithUpdateFields declares which fields are updatable. Column name defaults to the field name. Fields not declared here are rejected by Update.
func WithoutOwnerScope ¶
func WithoutOwnerScope() StoreOption
WithoutOwnerScope disables automatic OwnerScope for models embedding db.Owned. Use this when an owned model should be visible to all users.
type Submitter ¶
type Submitter interface {
SubmitFunc(ctx context.Context, name string, fn func(context.Context) error) error
}
Submitter is the interface for async task dispatch (satisfied by *scheduler.Pool). Kept minimal to avoid importing the scheduler package.
type UpdateOption ¶
type UpdateOption interface {
// contains filtered or unexported methods
}
UpdateOption tunes Update behaviour beyond what Changes expresses.
type VersionConflictError ¶
type VersionConflictError struct {
Locator string
Version int // the stale version the caller supplied
RID string
IDValue uint
HasRID bool
HasID bool
}
VersionConflictError carries the locator and the stale version that was supplied. Useful for logging and API error detail.
Locator is the redacted display form; RID / IDValue carry the concrete key (populated when the locator was store.RID / store.ID) for server-side diagnostics via errors.As. HasRID / HasID mirror the NotFoundError flags.
func (*VersionConflictError) Error ¶
func (e *VersionConflictError) Error() string
func (*VersionConflictError) Is ¶
func (e *VersionConflictError) Is(target error) bool
Is makes errors.Is(err, ErrStaleVersion) return true.
type Writer ¶
type Writer[T db.Modeler] interface { Create(ctx context.Context, obj *T) error Update(ctx context.Context, by Locator, changes Changes, opts ...UpdateOption) error Delete(ctx context.Context, by Locator, opts ...DeleteOption) error BatchCreate(ctx context.Context, objs []*T) error Upsert(ctx context.Context, obj *T, conflictColumns []string, updateColumns ...string) error }
Writer is a write-only view of a Store. Business code that only mutates data should depend on this interface rather than the full *Store[T].