entity

package
v0.91.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FormatValidationErrors

func FormatValidationErrors(errors map[string]string) []string

FormatValidationErrors formats a map of field errors into a user-friendly string slice.

func SeedDataFromContext

func SeedDataFromContext(ctx context.Context) ([]byte, error)

SeedDataFromContext returns the bytes referenced by the entity's SeedFS + SeedPath. Use inside a Seed function:

Seed: func(ctx context.Context, db *sql.DB) error {
    data, err := entity.SeedDataFromContext(ctx)
    if err != nil {
        return err
    }
    var rows []FoodRow
    if err := json.Unmarshal(data, &rows); err != nil {
        return err
    }
    // ...insert rows...
}

Returns an error when no SeedFS was configured on the EntityConfig. Name matches the framework convention (TxFromContext, SessionFromContext, RegistryFromContext); the older *FromCtx shape is a battery/auth outlier.

func ValidKey added in v0.87.0

func ValidKey(s string) bool

ValidKey reports whether s follows the Display key grammar: a lowercase ASCII slug starting with a letter. Code that names screen parts with keys checks them here. States move keys follow the stricter transitionKeyGrammar; see ValidateStates.

func ValidateStates added in v0.87.0

func ValidateStates(name string, cfg EntityConfig) (err error)

ValidateStates runs the states block's boot checks over an EntityConfig the way App.Entity would, for code generators that read hand-written entity declarations (entities/*.go through packReadEntities) which never passed registration. Define runs first so the guards judge the same field set the app judges — Define injects the framework-managed columns (timestamps, deleted_at, tenant and owner columns) the guards refuse. The states block is what it checks, but Define panics on other declaration errors (an undeclared SearchFields entry, a bad ReadScope predicate); those come back as the error too, so a generator reading a hand-edited spec fails naming the entity rather than with a stack trace.

func WithSeedDataContext

func WithSeedDataContext(ctx context.Context, sfs fs.FS, path string) context.Context

WithSeedDataContext attaches a SeedFS + SeedPath pair to ctx for retrieval by SeedDataFromContext inside a Seed function. The framework calls this internally; hosts should not need to invoke it directly.

Types

type AccessControl

type AccessControl struct {
	Read   string // List + Get
	Create string
	Update string
	Delete string
}

AccessControl declares the RBAC permission required for each CRUD operation on an entity. Each field holds a permission string (e.g. "posts:write"); blank means that operation is not RBAC-gated. Read covers both List and Get.

Permissions are plain strings here so the entity package stays decoupled from framework/access; the CRUD layer converts them to access.Permission and enforces them via access.Can against the policy + roles in the request context.

func (AccessControl) Declared added in v0.29.0

func (a AccessControl) Declared() bool

Declared reports whether any per-operation permission is set, i.e. whether the entity opted into RBAC gating at all. Used by framework/crud's secure-by-default session gate to tell "this entity declared an (possibly partial) access: block, defer to it as today" apart from "this entity declared nothing".

type AccessDeclaration added in v0.5.0

type AccessDeclaration struct {
	Read   string `json:"read,omitempty"`
	Create string `json:"create,omitempty"`
	Update string `json:"update,omitempty"`
	Delete string `json:"delete,omitempty"`
}

AccessDeclaration is the JSON/YAML-friendly mirror of AccessControl, the per-operation RBAC permissions for a blueprint-declared entity. "read" covers both List and Get. The CRUD layer enforces these via access.Can against the policy + roles in the request context (403 on missing permission), exactly like a Go-declared EntityConfig.Access.

type BoolColumn

type BoolColumn struct {
	// contains filtered or unexported fields
}

BoolColumn represents a BOOLEAN column.

func NewBoolColumn

func NewBoolColumn(name string) BoolColumn

func (BoolColumn) Asc

func (c BoolColumn) Asc() Order

func (BoolColumn) Desc

func (c BoolColumn) Desc() Order

func (BoolColumn) Eq

func (c BoolColumn) Eq(v bool) Condition

func (BoolColumn) IsFalse

func (c BoolColumn) IsFalse() Condition

func (BoolColumn) IsNotNull

func (c BoolColumn) IsNotNull() Condition

func (BoolColumn) IsNull

func (c BoolColumn) IsNull() Condition

func (BoolColumn) IsTrue

func (c BoolColumn) IsTrue() Condition

type CardFields added in v0.87.0

type CardFields struct {
	Title    string   `json:"title,omitempty"`
	Subtitle string   `json:"subtitle,omitempty"`
	Badge    string   `json:"badge,omitempty"`
	Meta     []string `json:"meta,omitempty"`
}

CardFields names the fields a card shows when a list is drawn as cards.

type Condition

type Condition struct {
	// contains filtered or unexported fields
}

Condition is a where-clause fragment plus its bound arguments.

func And

func And(conds ...Condition) Condition

And combines conditions with AND. Useful inside Or(...) to nest a group of ANDed predicates: Or(And(a, b), And(c, d)).

func Not

func Not(c Condition) Condition

Not wraps a condition in NOT (...).

func Or

func Or(conds ...Condition) Condition

Or combines conditions with OR. Each conjunct keeps its own internal argument order; placeholders are renumbered at QueryBuilder.Build time so "$1" in a fragment doesn't collide with another fragment's "$1".

func (Condition) Apply

func (c Condition) Apply(qb *query.QueryBuilder)

Apply appends this condition to the query builder.

func (Condition) Args

func (c Condition) Args() []any

Args returns the bound arguments for the fragment. Pair with SQL().

func (Condition) SQL

func (c Condition) SQL() string

SQL returns the where-clause fragment. Exported so callers outside the entity package (typed_query, etc.) can compose conditions onto query builders directly.

type DisplayConfig added in v0.87.0

type DisplayConfig struct {
	Singular string `json:"singular,omitempty"`
	Plural   string `json:"plural,omitempty"`
	// TitleFields name a record in lists, drawers, pickers and
	// breadcrumbs: their values joined with " · ". A Relation field
	// contributes the related record's own title, so a subscription
	// titled by customer_id and plan_id reads "Ada Lovelace · Pro". The
	// first carries the list's record link.
	TitleFields []string `json:"title_fields,omitempty"`
	Description string   `json:"description,omitempty"`
	Columns     []string `json:"columns,omitempty"`

	// Nav places the entity in the sidebar. Hide drops it from nav and the
	// dashboard; it never changes what the admin exposes.
	Nav    *EntityNav  `json:"nav,omitempty"`
	Views  []ListView  `json:"views,omitempty"`
	Facets []string    `json:"facets,omitempty"`
	Form   *EntityForm `json:"form,omitempty"`
	Card   *CardFields `json:"card,omitempty"`

	// Fields holds per-field screen hints keyed by field name.
	Fields map[string]FieldDisplay `json:"fields,omitempty"`

	// PageSizes lists the choices the page-size menu offers. Each entry must
	// be positive and within Pagination.MaxListLimit when that is set.
	PageSizes []int `json:"page_sizes,omitempty"`

	// NoDuplicate and NoBulk turn off the Duplicate row action, or every
	// bulk action, for this entity. The No prefix matches NoQuery.
	NoDuplicate bool `json:"no_duplicate,omitempty"`
	NoBulk      bool `json:"no_bulk,omitempty"`
}

DisplayConfig holds the screen hints for one entity: the names, columns, views, form layout and nav placement the admin and generated screens read. It is plain data; nil (the default) means every screen falls back to the schema itself. It never changes what the API accepts or returns: a field's own Hidden, ReadOnly and NoQuery flags keep their meaning and screens honour them first.

Where the entity already says something (search fields, page limits, who may write), Display reads it rather than repeating it. Strings that hold a query (a view's Where and Sort, a field's ShowWhen) are not parsed here: this package cannot import the query DSL, so App.Entity and GroupEntity parse them when the app registers the entity.

func (*DisplayConfig) UnmarshalJSON added in v0.87.0

func (d *DisplayConfig) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the display block strictly: an unknown key is an error, not a silent ignore, matching how the blueprint decoder refuses unknown keys in every other entity group.

type Endpoint

type Endpoint struct {
	Method       string          `json:"method"`
	Path         string          `json:"path"`
	Name         string          `json:"name,omitempty"`
	Description  string          `json:"description,omitempty"`
	MCP          bool            `json:"mcp,omitempty"`
	InputSchema  []schema.Field  `json:"inputSchema,omitempty"`
	OutputSchema []schema.Field  `json:"outputSchema,omitempty"`
	Handler      http.Handler    `json:"-"`
	MCPHandler   mcp.ToolHandler `json:"-"`

	// MCPGate is an optional per-caller precondition for the MCP twin. It
	// runs before MCPHandler on every tools/call, and decides whether the
	// tool is visible to that caller in tools/list.
	//
	// It exists because the two front doors of one Endpoint do not get the
	// same protection for free: Handler inherits the route's middleware
	// chain, while MCPHandler is registered straight onto the MCP server and
	// sees none of it. An endpoint behind auth.RequireRole("editor") was
	// therefore role-checked over HTTP and ungated over MCP.
	//
	// When unset, the twin defaults to requiring an authenticated caller
	// (framework.MCPRequireUser). Set MCPPublic to opt out of that default;
	// set this to something stricter, e.g. auth.MCPRole("editor").
	MCPGate func(ctx context.Context) error `json:"-"`

	// MCPPublic opts the MCP twin out of the default authenticated-caller
	// gate, for an endpoint that really is anonymous over HTTP too. Ignored
	// when MCPGate is set.
	MCPPublic bool `json:"mcpPublic,omitempty"`
}

Endpoint declares a custom route owned by an entity.

Path may be absolute ("/posts/{id}/publish") or relative to the entity table path ("{id}/publish"). Both Go 1.22 "{id}" and older ":id" parameter syntax are accepted. Handler is used for HTTP. MCPHandler is optional and is only registered when MCP is true.

Under framework.WithAPIPrefix a relative path resolves under the prefixed table path. WithAPIPrefix("/api") mounts "{id}/publish" on entity "posts" at POST /api/posts/{id}/publish, alongside that entity's CRUD routes. An absolute path bypasses the prefix entirely; use it to mount outside the entity's API namespace.

InputSchema and OutputSchema are OPTIONAL typed descriptions of the request body and the success (200) response, expressed as []schema.Field, the same representation the entity's own CRUD schema is built from, so OpenAPI and the generated MCP tool both consume one source. When unset (nil), the endpoint renders exactly as before: a shapeless {type:object} request/response in OpenAPI and a {type:object} MCP tool input schema. InputSchema is ignored for GET endpoints (which carry no request body).

type Entity

type Entity struct {
	Config     EntityConfig
	DB         *sql.DB
	PrimaryKey string // defaults to "id"

	// Version identifies the API version this entity is mounted under, when
	// registered via App.GroupEntity. It is the route group's full prefix
	// (e.g. "/api/v1"). Empty for entities registered via App.Entity, those
	// keep the historical single-version behaviour. The registry keys on
	// (Config.Name, Version) so the same entity name can coexist under
	// different versions; callers that don't care about version resolve the
	// unversioned or sole entity via Registry.Get.
	Version string

	// OpenAPITag is the tag applied to this entity's operations in the
	// generated OpenAPI document. Set from the route group's OpenAPITag
	// when registered via App.GroupEntity; empty for App.Entity (the tag
	// defaults to the entity name in that case).
	OpenAPITag string
	// contains filtered or unexported fields
}

Entity represents a registered domain entity with its config and DB handle.

func Define

func Define(name string, config EntityConfig) *Entity

Define creates a new Entity with the given name and configuration. It applies defaults (Table, Timestamps=true) and stores the name. It also injects system fields (id, timestamps, and the scope-driven tenant/owner/soft-delete columns) with AutoGenerate flags unless the user has already defined them.

func ResolveTarget added in v0.48.0

func ResolveTarget(reg Registry, source *Entity, targetName string) (*Entity, error)

ResolveTarget resolves a relation target for a source entity, preferring the target registered at the SOURCE's own version.

Relation resolution drives the Hidden-column scrub, the owner/tenant scopes, the soft-delete filter and the scoped-filter allow-list. Resolving by name alone picks the unversioned declaration when one exists, so a request under /api/v1 could inherit an internal entity's visibility and scoping rules instead of v1's, disclosing columns v1 marks hidden and returning rows v1's scopes exclude.

Order: the source's own version first; then unversioned, which is the shared declaration a versioned entity legitimately points at; then a sole version when exactly one exists. Ambiguity is an ERROR, never a silent pick, the caller must fail closed.

func (*Entity) Audited added in v0.87.0

func (e *Entity) Audited() bool

Audited reports whether MarkAudited was called.

func (*Entity) GetFields

func (e *Entity) GetFields() []schema.Field

GetFields returns the entity's field definitions.

func (*Entity) GetName

func (e *Entity) GetName() string

GetName returns the entity name.

func (*Entity) GetTable

func (e *Entity) GetTable() string

GetTable returns the DB table name.

func (*Entity) MarkAudited added in v0.87.0

func (e *Entity) MarkAudited()

MarkAudited records that an audit log writes this entity's rows. framework.WithAuditLog calls it for each entity it records; a state override (crud.WithStateOverride) is refused on an entity it never marked, since the override's only trail is the audit row.

func (*Entity) Schema

func (e *Entity) Schema() schema.Schema

Schema returns a core/schema.Schema built from the entity's fields.

func (*Entity) SetDB

func (e *Entity) SetDB(db *sql.DB)

SetDB sets the database connection for this entity.

func (*Entity) String

func (e *Entity) String() string

String implements fmt.Stringer.

func (*Entity) Validate

func (e *Entity) Validate() error

Validate checks that the entity config is well-formed.

type EntityConfig

type EntityConfig struct {
	Name       string            // entity name (e.g. "users")
	Table      string            // DB table name (defaults to snake_case of Name)
	Fields     []schema.Field    // typed field definitions
	Relations  []Relation        // entity relationships
	Endpoints  []Endpoint        // custom HTTP endpoints for this entity
	Scope      *ScopeConfig      // ownership, tenancy, and soft-delete behavior
	Pagination *PaginationConfig // list limits and keyset cursor shape
	Exposure   *ExposureConfig   // generated HTTP/MCP and access posture
	Timestamps *bool             // add created_at / updated_at columns; nil defaults to true
	Indices    []Index           // additional CREATE INDEX statements emitted by AutoMigrate
	Unmanaged  bool              // when true, the migration system never emits DDL for this object (it is created elsewhere, e.g. a view, an FTS virtual table, or a legacy/external table). The ORM still queries it.

	// Properties holds caller-owned metadata. The framework does not
	// interpret any key; generators, plugins, and apps define their own keys.
	Properties map[string]any

	// SearchFields names the DB columns that ?q= free-text search operates
	// on (e.g. []string{"title","body"}). When non-empty, a List request
	// carrying ?q=<term> tokenizes the term on whitespace (deduped, capped
	// at filter.MaxSearchTerms) and AND-composes one LOWER(col) LIKE
	// pattern per token across the listed fields. Matching is
	// ASCII-case-insensitive everywhere (Unicode-folding on Postgres).
	// Leave nil to keep pre-existing behaviour (?q= is ignored). Column
	// names must be known, non-Hidden, and String/Text-typed; Define panics
	// otherwise. An entity WITHOUT SearchFields ignores ?q= exactly as
	// before (back-compat).
	SearchFields []string

	// Seed runs once per entity after AutoMigrate creates the table. The
	// framework tracks completion in the _gofastr_seeded ledger; subsequent
	// App.Start() calls skip the entity. Errors abort App.Start.
	//
	// Go-only: function values cannot be expressed in a blueprint
	// declaration. Apps whose entities come from a gofastr.yml blueprint
	// must wire seeding from Go.
	//
	// Concurrency: RunSeeds is NOT safe for concurrent invocation across
	// multiple processes. The framework assumes serialized startup (one
	// process / replica calls App.Start at a time). For HA setups, gate
	// seeding behind an external mechanism (init container, one-shot
	// job, advisory lock). Seed implementations should be idempotent
	// (INSERT … ON CONFLICT DO NOTHING) so accidental re-runs cannot
	// duplicate data.
	Seed func(ctx context.Context, db *sql.DB) error

	// SeedFS is an optional fs.FS (typically a //go:embed embed.FS) that
	// the framework attaches to the Seed function's context. Use with
	// SeedPath to point at a single file within the FS.
	//
	// Go-only: like Seed itself, an fs.FS cannot be expressed in JSON
	// entity declarations.
	SeedFS fs.FS

	// SeedPath is the path within SeedFS that Seed should consume.
	// Ignored when SeedFS is nil.
	SeedPath string

	// LenientFilters opts the entity's auto-CRUD List endpoint OUT of strict
	// filter parsing. By default an unknown top-level filter key (a typo like
	// ?stauts=active) is REJECTED with a 400 rather than silently dropped,
	// silently dropping it returns an UNFILTERED result set, which is a
	// data-exposure and broken-client hazard. Set true only as a migration
	// escape hatch for an endpoint that must tolerate arbitrary extra query
	// params (e.g. legacy tracking params); prefer fixing the caller.
	// Default false (strict).
	LenientFilters bool

	// AllowedFilterParams declares extra query-param keys that are NOT entity
	// columns but are legitimately consumed elsewhere on the List request,
	// typically read by a BeforeList hook or custom middleware (e.g. a
	// bespoke "?region=eu" scope param). Strict filter parsing skips these
	// instead of rejecting them, so the endpoint keeps typo-protection for
	// real fields without falling back to LenientFilters (which disables it
	// entirely). Reserved list controls are always allowed and need not be
	// listed here.
	AllowedFilterParams []string

	// Display holds the screen hints for this entity: names, columns,
	// views, form layout, nav placement and per-field drawing hints. It is
	// a pointer config like Scope: nil means every default, and Define
	// copies it deeply so a caller that edits its value afterwards changes
	// nothing the app checked or serves. It never widens what the API
	// accepts; see DisplayConfig for the boot check over every name it
	// holds and where the query strings inside it are parsed.
	Display *DisplayConfig
	// States names the Enum field holding the record's state and the
	// moves that change it. Unless Advisory is set the CRUD handler
	// refuses any write that changes the field or a stamp outside a move;
	// see StatesConfig. Nil means no states. Define copies it deeply.
	States *StatesConfig
	// Renames declares column renames (old name → new name) so the schema
	// diff emits a non-destructive ALTER TABLE … RENAME COLUMN instead of a
	// data-losing DROP of the old column + ADD of the new one. Rename is
	// otherwise indistinguishable from drop+add, so it requires this explicit
	// declaration; auto-detection is unsafe. A rename only fires when the old
	// column is present in the live schema and the new name is declared on the
	// entity. Declare it in Go or under an entity's `renames:` key in a
	// blueprint.
	Renames map[string]string
}

EntityConfig holds the declarative configuration for an entity. Name is set via Define(); Fields declare the schema. Timestamps is nil by default; Define resolves nil to true.

func (EntityConfig) TenantColumn

func (c EntityConfig) TenantColumn() string

TenantColumn returns the tenant-scoping column name for this entity: Scope.TenantField when set, otherwise "tenant_id".

func (EntityConfig) WithTimestamps

func (c EntityConfig) WithTimestamps(v bool) EntityConfig

WithTimestamps returns a copy with timestamp columns enabled or disabled.

type EntityDeclaration

type EntityDeclaration struct {
	Name         string                 `json:"name"`
	Table        string                 `json:"table,omitempty"`
	Fields       []FieldDeclaration     `json:"fields"`
	Relations    []Relation             `json:"relations,omitempty"`
	Endpoints    []Endpoint             `json:"endpoints,omitempty"`
	Scope        *ScopeDeclaration      `json:"scope,omitempty"`
	Pagination   *PaginationDeclaration `json:"pagination,omitempty"`
	Exposure     *ExposureDeclaration   `json:"exposure,omitempty"`
	SearchFields []string               `json:"search_fields,omitempty"`
	Timestamps   *bool                  `json:"timestamps,omitempty"`
	Indices      []Index                `json:"indices,omitempty"`
	Properties   map[string]any         `json:"properties,omitempty"`
	Renames      map[string]string      `json:"renames,omitempty"`
	// Display is the declaration mirror of EntityConfig.Display, spelled
	// `display` under an entity with the snake_case keys documented on
	// DisplayConfig. Unlike the other groups it has no flat shorthand:
	// every key lives under display. Unknown keys inside it are refused.
	Display *DisplayConfig `json:"display,omitempty"`
	// States is the declaration mirror of EntityConfig.States, spelled
	// `states` with the keys documented on StatesConfig. Unknown keys
	// inside it are refused.
	States *StatesConfig `json:"states,omitempty"`
}

EntityDeclaration is the grouped JSON/YAML shape used after blueprint decoding. The decoder also accepts flat shorthand keys and moves them into Scope, Pagination, or Exposure before returning the declaration.

func (EntityDeclaration) Config

func (d EntityDeclaration) Config() (EntityConfig, error)

Config converts a declaration into an EntityConfig.

func (*EntityDeclaration) UnmarshalJSON added in v0.54.0

func (d *EntityDeclaration) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts grouped declarations and the documented flat shorthand. A flat key and its grouped key may both be present only when their values match.

type EntityForm added in v0.87.0

type EntityForm struct {
	Main []FormItem `json:"main,omitempty"`
	Side []FormItem `json:"side,omitempty"`
}

EntityForm says where fields sit on the record: a main column and a narrow side column. Fields the form leaves out are appended at the end of Main in schema order (minus the auto-generated system fields), so adding a field never makes it vanish. No form at all means one column in schema order.

type EntityNav added in v0.87.0

type EntityNav struct {
	Group     string `json:"group,omitempty"`
	Icon      string `json:"icon,omitempty"`
	Order     int    `json:"order,omitempty"`
	Hide      bool   `json:"hide,omitempty"`
	HideCount bool   `json:"hide_count,omitempty"`
}

EntityNav places an entity (or an admin page) in the sidebar: under a group, with an icon, in an order. Group is a key — a lowercase ASCII slug, never `all` or `deleted` — translated as nav.groups.<group>. Hide drops the entity from nav and the dashboard; it never changes what the admin exposes. An entity's nav row shows how many records the viewer can read, recounted on every navigation; HideCount drops the count for a table too large to count per click. Pages ignore it.

type EnumColumn added in v0.91.0

type EnumColumn[T ~string] struct {
	// contains filtered or unexported fields
}

EnumColumn represents an enum column whose values codegen emits as typed constants of T: PostsStatus.Eq(PostsStatusLive). Stored as TEXT, like StringColumn; T keeps a plain string variable out of the comparison.

func NewEnumColumn added in v0.91.0

func NewEnumColumn[T ~string](name string) EnumColumn[T]

NewEnumColumn constructs an EnumColumn for the given DB column name. Codegen calls this; user code rarely needs to.

func (EnumColumn) Asc added in v0.91.0

func (c EnumColumn) Asc() Order

func (EnumColumn) Desc added in v0.91.0

func (c EnumColumn) Desc() Order

func (EnumColumn[T]) Eq added in v0.91.0

func (c EnumColumn[T]) Eq(v T) Condition

func (EnumColumn[T]) In added in v0.91.0

func (c EnumColumn[T]) In(values ...T) Condition

func (EnumColumn) IsNotNull added in v0.91.0

func (c EnumColumn) IsNotNull() Condition

func (EnumColumn) IsNull added in v0.91.0

func (c EnumColumn) IsNull() Condition

func (EnumColumn[T]) Neq added in v0.91.0

func (c EnumColumn[T]) Neq(v T) Condition

type ExposureConfig added in v0.41.0

type ExposureConfig struct {
	CRUD      *bool            // nil or true generates CRUD routes
	MCP       bool             // register MCP CRUD tools
	Public    bool             // allow anonymous CRUD when no scope or access rule applies
	Access    AccessControl    // per-operation permissions
	ReadScope *ReadScopeConfig // narrows which rows a caller may read; nil = no row filtering
}

ExposureConfig groups generated routes and their access rules. CRUD is a pointer so nil keeps automatic route generation and false disables it.

type ExposureDeclaration added in v0.41.0

type ExposureDeclaration struct {
	CRUD      *bool                 `json:"crud,omitempty"`
	MCP       bool                  `json:"mcp,omitempty"`
	Public    bool                  `json:"public,omitempty"`
	Access    *AccessDeclaration    `json:"access,omitempty"`
	ReadScope *ReadScopeDeclaration `json:"read_scope,omitempty"`
}

ExposureDeclaration is the JSON/YAML-friendly shape of ExposureConfig.

type FieldDeclaration

type FieldDeclaration struct {
	Name         string   `json:"name"`
	Type         string   `json:"type"`
	Required     bool     `json:"required,omitempty"`
	Unique       bool     `json:"unique,omitempty"`
	Default      any      `json:"default,omitempty"`
	AutoGenerate string   `json:"auto_generate,omitempty"`
	ReadOnly     bool     `json:"read_only,omitempty"`
	Hidden       bool     `json:"hidden,omitempty"`
	NoQuery      bool     `json:"no_query,omitempty"`
	Max          *float64 `json:"max,omitempty"`
	Min          *float64 `json:"min,omitempty"`
	Pattern      string   `json:"pattern,omitempty"`
	Values       []string `json:"values,omitempty"`
	To           string   `json:"to,omitempty"`
	Many         bool     `json:"many,omitempty"`
}

FieldDeclaration is a JSON-friendly schema.Field.

func (FieldDeclaration) Field

func (fd FieldDeclaration) Field() (schema.Field, error)

Field converts a JSON field declaration into schema.Field.

type FieldDisplay added in v0.87.0

type FieldDisplay struct {
	Label       string `json:"label,omitempty"`
	Help        string `json:"help,omitempty"`
	Placeholder string `json:"placeholder,omitempty"`

	// Locked draws the field read-only on screens. The screens' save
	// path drops a Locked key before the write; the API may still write
	// it. Refused at registration on a Required field with no Default:
	// the value never submits on create, so no form could create the
	// record.
	Locked bool `json:"locked,omitempty"`

	// Omit leaves the field out of forms and columns on purpose. Unlike
	// Hidden, the API still returns it. Refused at registration on a
	// Required field with no Default: no form could create the record.
	Omit bool `json:"omit,omitempty"`

	// ShowWhen shows the field only while `field = value` or
	// `field in [...]` holds on an editable Enum or Bool field. Parsed
	// with the query DSL when App.Entity registers the entity; any
	// other shape is refused there. A hidden region's controls are
	// disabled, so they never submit — refused at registration on a
	// Required field with no Default, like Omit.
	ShowWhen string `json:"show_when,omitempty"`

	// Input picks a different input and cell for the same storage: a
	// built-in kind (email, url, color, markdown, code) or one the app
	// registers in entityui.Extensions.Kinds. A key here; whether the
	// kind exists is checked when entityui.New builds the app's UI.
	Input string `json:"input,omitempty"`
}

FieldDisplay is the per-field screen hint: how a field is labelled and drawn, never what the API accepts.

type FloatColumn

type FloatColumn struct {
	// contains filtered or unexported fields
}

FloatColumn represents a REAL/DOUBLE PRECISION/DECIMAL column.

func NewFloatColumn

func NewFloatColumn(name string) FloatColumn

func (FloatColumn) Asc

func (c FloatColumn) Asc() Order

func (FloatColumn) Desc

func (c FloatColumn) Desc() Order

func (FloatColumn) Eq

func (c FloatColumn) Eq(v float64) Condition

func (FloatColumn) Gt

func (c FloatColumn) Gt(v float64) Condition

func (FloatColumn) Gte

func (c FloatColumn) Gte(v float64) Condition

func (FloatColumn) IsNotNull

func (c FloatColumn) IsNotNull() Condition

func (FloatColumn) IsNull

func (c FloatColumn) IsNull() Condition

func (FloatColumn) Lt

func (c FloatColumn) Lt(v float64) Condition

func (FloatColumn) Lte

func (c FloatColumn) Lte(v float64) Condition

func (FloatColumn) Neq

func (c FloatColumn) Neq(v float64) Condition

type FormItem added in v0.87.0

type FormItem struct {
	Field     string     `json:"-"`
	Row       []string   `json:"row,omitempty"`
	Section   string     `json:"section,omitempty"`
	Help      string     `json:"help,omitempty"`
	Collapsed bool       `json:"collapsed,omitempty"`
	Items     []FormItem `json:"items,omitempty"`
}

FormItem is one row of the form: exactly one of Field (one field at the column's full width), Row (one to three fields side by side) or Section (a heading over its Items, optionally starting Collapsed with a Help line). In YAML a field item is a bare string; sections nest at most two deep.

func (FormItem) MarshalJSON added in v0.87.0

func (fi FormItem) MarshalJSON() ([]byte, error)

MarshalJSON emits the same three shapes UnmarshalJSON reads: a field item as a bare string, a row or section as an object, so a decoded form round-trips.

func (*FormItem) UnmarshalJSON added in v0.87.0

func (fi *FormItem) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the three form-item shapes: a bare string (one field), {"row": [...]}, or {"section": key, "help": ..., "collapsed": ..., "items": [...]} where items takes the same three shapes again. Any other key is refused.

type Index

type Index struct {
	Name       string   `json:"name,omitempty"`
	Columns    []string `json:"columns,omitempty"`
	Unique     bool     `json:"unique,omitempty"`
	Expression string   `json:"expression,omitempty"`
}

Index declares a secondary index on an entity. Both dialects accept the same CREATE INDEX syntax; AutoMigrate emits CREATE INDEX IF NOT EXISTS so re-runs are safe.

Name is optional, when empty, AutoMigrate synthesises one as "idx_<table>_<col1>_<col2>". Unique indices reject duplicate rows for the chosen column set; for single-column uniqueness prefer the Field-level Unique flag which lives on the column definition.

Expression covers the case the column-list form can't express: a functional or partial index, e.g. `UNIQUE(user_id, lower(food))` to dedupe case-insensitively. When non-empty, Expression is rendered verbatim inside the index body (replacing Columns). Name is REQUIRED in that case because there's no safe deterministic slug for an arbitrary expression. Use Columns for plain identifier indices; reach for Expression when SQL functions or constants need to participate in the indexed key.

type IntColumn

type IntColumn struct {
	// contains filtered or unexported fields
}

IntColumn represents an INTEGER column.

func NewIntColumn

func NewIntColumn(name string) IntColumn

func (IntColumn) Asc

func (c IntColumn) Asc() Order

func (IntColumn) Desc

func (c IntColumn) Desc() Order

func (IntColumn) Eq

func (c IntColumn) Eq(v int) Condition

func (IntColumn) Gt

func (c IntColumn) Gt(v int) Condition

func (IntColumn) Gte

func (c IntColumn) Gte(v int) Condition

func (IntColumn) In

func (c IntColumn) In(values ...int) Condition

func (IntColumn) IsNotNull

func (c IntColumn) IsNotNull() Condition

func (IntColumn) IsNull

func (c IntColumn) IsNull() Condition

func (IntColumn) Lt

func (c IntColumn) Lt(v int) Condition

func (IntColumn) Lte

func (c IntColumn) Lte(v int) Condition

func (IntColumn) Neq

func (c IntColumn) Neq(v int) Condition

type ListView added in v0.87.0

type ListView struct {
	Key     string `json:"key"`
	Label   string `json:"label,omitempty"`
	Where   string `json:"where,omitempty"`
	Sort    string `json:"sort,omitempty"`
	As      string `json:"as,omitempty"`
	Default bool   `json:"default,omitempty"`
}

ListView is a named starting point for a list: a DSL Where and Sort, an optional As for how rows are drawn ("table", "cards"), and Default for the one view that opens when the URL names none. A view whose filter depends on the caller names no Where; its func is registered under the view's key next to the record screens, and an unregistered Where-less view fails there, at app start.

type OnDeleteAction added in v0.87.0

type OnDeleteAction string

OnDeleteAction defines the referential action when a referenced row is deleted.

const (
	OnDeleteNoAction OnDeleteAction = "NO ACTION"
	OnDeleteRestrict OnDeleteAction = "RESTRICT"
	OnDeleteCascade  OnDeleteAction = "CASCADE"
	OnDeleteSetNull  OnDeleteAction = "SET NULL"
)

type Order

type Order struct {
	// contains filtered or unexported fields
}

Order is an order-by-clause fragment.

func (Order) Apply

func (o Order) Apply(qb *query.QueryBuilder)

Apply appends this order to the query builder.

type PaginationConfig added in v0.41.0

type PaginationConfig struct {
	CursorField  string
	CursorFields []string
	MaxListLimit int
}

PaginationConfig groups list limits and keyset cursor configuration. A non-empty CursorFields composite takes precedence over CursorField.

type PaginationDeclaration added in v0.41.0

type PaginationDeclaration struct {
	CursorField  string   `json:"cursor_field,omitempty"`
	CursorFields []string `json:"cursor_fields,omitempty"`
	MaxListLimit int      `json:"max_list_limit,omitempty"`
}

PaginationDeclaration is the JSON/YAML-friendly shape of PaginationConfig.

type ReadScopeConfig added in v0.68.0

type ReadScopeConfig struct {
	Filter       []RowPredicate
	Unrestricted string
}

ReadScopeConfig narrows WHICH rows a caller may read, as opposed to Exposure.Access, which decides whether they may read the entity at all. Filter's predicates are AND-ed into every read of the entity's own table (List, Get, count, cursor, stream, the in-process API, typed queries, and the ?include= / eager-load paths when this entity is the relation target) unless the caller is unrestricted. Writes (update, delete, the upsert write) are NOT filtered in this version, but the row an update or upsert hands back is: a row outside the caller's scope comes back as its id only.

Unrestricted names a permission: a caller holding it reads every row. When Unrestricted is EMPTY the lift is weaker and deliberate: any caller with a session reads every row, and an anonymous caller gets the filter — the "anonymous visitors see published rows, signed-in editors see drafts" posture. "Any signed-in user" is exactly what it says; it is not an editor role, so say so in the declaration when that matters.

The predicates AND together; there is no OR form in this version.

type ReadScopeDeclaration added in v0.68.0

type ReadScopeDeclaration struct {
	Filter       []RowPredicateDeclaration `json:"filter,omitempty"`
	Unrestricted string                    `json:"unrestricted,omitempty"`
}

ReadScopeDeclaration is the JSON/YAML-friendly mirror of ReadScopeConfig — the row filter narrowing WHICH rows a caller may read, as opposed to Access, which decides whether they may read the entity at all. Unrestricted names a permission that lifts the filter; empty means any caller with a session reads every row while an anonymous caller gets the filter.

type Registry

type Registry interface {
	// All returns a snapshot of every registered entity keyed by name.
	// Map iteration order is randomised by Go; for stable iteration use
	// AllSorted().
	All() map[string]*Entity

	// AllSorted returns every registered entity in alphabetical order
	// by name. Use this when emitting bytes whose ordering matters
	// (OpenAPI, generated code, golden-file tests, ETag-cached
	// responses).
	AllSorted() []*Entity

	// Get retrieves one entity by name, or an error when no such entity
	// is registered. When multiple versions of the name exist and none is
	// unversioned, Get returns an ambiguity error.
	//
	// Get is NOT safe for resolving a relation target: it prefers the
	// unversioned entity, so a v1 relation would resolve an unversioned
	// declaration with different Hidden columns and different scopes. Use
	// ResolveTarget for anything that drives visibility or scoping.
	Get(name string) (*Entity, error)
}

Registry is the minimal contract subpackages need from the framework's entity registry: enumerate every registered entity.

All() returns the entities keyed by name. Go's map iteration is randomised, so callers that emit order-sensitive output (OpenAPI tags, LLM markdown, generated code) must use AllSorted() to keep output stable across runs. Callers that only care about presence (counts, hash lookups, contains-checks) can use All() directly.

The concrete *framework.Registry type satisfies this implicitly. Splitting it out here lets framework/migrate, framework/dsl, and others depend on the entity model without pulling in the full framework package.

type Relation

type Relation struct {
	Type             RelationType   `json:"type"`
	Name             string         `json:"name"`                // logical name for this relation (e.g. "author", "comments")
	Entity           string         `json:"entity"`              // target entity/table name
	ForeignKey       string         `json:"foreign_key"`         // FK column name
	Through          string         `json:"through,omitempty"`   // pivot table name (ManyToMany only)
	LocalKey         string         `json:"local_key,omitempty"` // column on the local side of a ManyToMany pivot
	ForeignKeyTarget string         `json:"foreign_key_target,omitempty"`
	OnDelete         OnDeleteAction `json:"on_delete,omitempty"`     // referential action on delete (CASCADE, SET NULL, RESTRICT, NO ACTION)
	CascadeWrite     bool           `json:"cascade_write,omitempty"` // allows atomic parent-child creation/updates
}

Relation describes a relationship between two entities.

func BelongsTo

func BelongsTo(name, ent, foreignKey string) Relation

BelongsTo declares a many-to-one relationship. The source entity holds a foreign-key column that references the target entity's primary key.

func HasMany

func HasMany(name, ent, foreignKey string) Relation

HasMany declares a one-to-many relationship. The target entity holds a foreign-key column that references the source entity's primary key.

func HasOne

func HasOne(name, ent, foreignKey string) Relation

HasOne declares a one-to-one relationship. The target entity holds a foreign-key column that references the source entity's primary key.

func ManyToMany

func ManyToMany(name, ent, throughTable, sourceFK, targetFK string) Relation

ManyToMany declares a many-to-many relationship through a pivot/join table.

func (Relation) Cascade added in v0.87.0

func (r Relation) Cascade() Relation

Cascade enables atomic cascade writes for this relation.

func (Relation) OnDeleteAction added in v0.87.0

func (r Relation) OnDeleteAction(action OnDeleteAction) Relation

OnDeleteAction sets the ON DELETE referential action.

func (Relation) OnDeleteCascade added in v0.87.0

func (r Relation) OnDeleteCascade() Relation

OnDeleteCascade configures the relation foreign key with ON DELETE CASCADE.

func (Relation) WithCascadeWrite added in v0.87.0

func (r Relation) WithCascadeWrite(enable bool) Relation

WithCascadeWrite sets whether cascade writes are enabled.

type RelationType

type RelationType int

RelationType enumerates the kinds of entity relationships.

const (
	RelHasOne     RelationType = iota // target has a FK pointing back to us
	RelHasMany                        // target has a FK pointing back to us (many rows)
	RelManyToOne                      // we hold a FK pointing to the target (BelongsTo)
	RelManyToMany                     // linked through a pivot/join table
)

type RowPredicate added in v0.68.0

type RowPredicate struct {
	Field  string   // column name, exactly as declared on the entity
	Op     string   // "eq", "neq", "in", "not_in"; empty means "eq"
	Value  string   // single-value ops
	Values []string // "in" / "not_in"
}

RowPredicate is one declared condition on the entity's own columns, used by ReadScopeConfig to narrow which rows a caller may read.

type RowPredicateDeclaration added in v0.68.0

type RowPredicateDeclaration struct {
	Field  string   `json:"field"`
	Op     string   `json:"op,omitempty"`
	Value  string   `json:"value,omitempty"`
	Values []string `json:"values,omitempty"`
}

RowPredicateDeclaration is the JSON/YAML-friendly mirror of RowPredicate. Op is one of "eq" (the default; empty means eq), "neq", "in", "not_in". Single-value ops carry Value; "in"/"not_in" carry Values.

type ScopeConfig added in v0.41.0

type ScopeConfig struct {
	SoftDelete     bool   // add deleted_at and hide deleted rows by default
	MultiTenant    bool   // scope rows to the tenant in the request context
	TenantField    string // tenant column; empty uses tenant_id
	OwnerField     string // owner column stamped and scoped by auto-CRUD
	CrossOwnerRead string // permission that lifts owner scoping for reads only
}

ScopeConfig groups the rules that constrain which rows a request can read or change. Define always populates EntityConfig.Scope on the resolved entity.

type ScopeDeclaration added in v0.41.0

type ScopeDeclaration struct {
	SoftDelete     bool   `json:"soft_delete,omitempty"`
	MultiTenant    bool   `json:"multi_tenant,omitempty"`
	TenantField    string `json:"tenant_field,omitempty"`
	OwnerField     string `json:"owner_field,omitempty"`
	CrossOwnerRead string `json:"cross_owner_read,omitempty"`
}

ScopeDeclaration is the JSON/YAML-friendly shape of ScopeConfig.

type StatesConfig added in v0.87.0

type StatesConfig struct {
	// Field is the Enum field holding the state.
	Field string `json:"field"`
	// Initial lists the values a create may set. Empty means the field's
	// Default alone; a field with neither is refused at boot.
	Initial []string `json:"initial,omitempty"`
	// Transitions are the moves, in the order screens draw them.
	Transitions []Transition `json:"transitions,omitempty"`
	// Advisory releases the state field and the stamps to direct writes,
	// today's behaviour, and keeps the moves as calls and buttons. For an
	// entity whose status is not a business rule.
	Advisory bool `json:"advisory,omitempty"`
}

StatesConfig names an entity's state field and the moves that change it. It is a rule the CRUD handler enforces, not a screen hint: unless Advisory is set, the state field and every stamp change only through CrudHandler.RunTransition, whatever the write comes from (REST, MCP, batch, cascade, upsert, a typed repo, a hook). A write that sends the stored value back is not a change and passes, so a PUT that round-trips the whole row still works. Trusted Go code that must write the field directly (seeds, imports, repair jobs) wraps its context in crud.WithStateOverride, which is audited and refused on an entity with no audit log.

Like Scope and Display it is a pointer config: nil means the entity has no states, and Define copies it deeply.

func (*StatesConfig) Guarded added in v0.87.0

func (s *StatesConfig) Guarded() []string

Guarded returns the state field followed by every distinct stamp field, the columns a write may not change outside a move.

func (*StatesConfig) InitialValues added in v0.87.0

func (s *StatesConfig) InitialValues(fields []schema.Field) []string

InitialValues returns Initial, or the field's Default when Initial is empty.

func (*StatesConfig) Open added in v0.87.0

func (s *StatesConfig) Open(value string) []string

Open returns the keys of the moves whose From holds value, in declared order, System moves included.

func (*StatesConfig) Transition added in v0.87.0

func (s *StatesConfig) Transition(key string) (Transition, bool)

Transition returns the move named key, or false.

func (*StatesConfig) UnmarshalJSON added in v0.87.0

func (s *StatesConfig) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the states block strictly, moves included: an unknown key is an error, as it is in the display block.

type StringColumn

type StringColumn struct {
	// contains filtered or unexported fields
}

StringColumn represents a TEXT/VARCHAR column. Use the methods to build Conditions: PostsTitle.Eq("hello"), PostsTitle.Like("%foo%"), etc.

func NewStringColumn

func NewStringColumn(name string) StringColumn

NewStringColumn constructs a StringColumn for the given DB column name. Codegen calls this; user code rarely needs to.

func (StringColumn) Asc

func (c StringColumn) Asc() Order

func (StringColumn) Desc

func (c StringColumn) Desc() Order

func (StringColumn) Eq

func (c StringColumn) Eq(v string) Condition

func (StringColumn) In

func (c StringColumn) In(values ...string) Condition

func (StringColumn) IsNotNull

func (c StringColumn) IsNotNull() Condition

func (StringColumn) IsNull

func (c StringColumn) IsNull() Condition

func (StringColumn) Like

func (c StringColumn) Like(pattern string) Condition

func (StringColumn) Neq

func (c StringColumn) Neq(v string) Condition

func (StringColumn) NotLike

func (c StringColumn) NotLike(pattern string) Condition

type TimestampColumn

type TimestampColumn struct {
	// contains filtered or unexported fields
}

TimestampColumn represents a TIMESTAMP/TIMESTAMPTZ column. Method semantics mirror IntColumn but accept any value the driver knows how to bind (time.Time, RFC3339 strings, etc.) so callers don't have to choose a canonical form here.

func NewTimestampColumn

func NewTimestampColumn(name string) TimestampColumn

func (TimestampColumn) Asc

func (c TimestampColumn) Asc() Order

func (TimestampColumn) Desc

func (c TimestampColumn) Desc() Order

func (TimestampColumn) Eq

func (c TimestampColumn) Eq(v any) Condition

func (TimestampColumn) Gt

func (c TimestampColumn) Gt(v any) Condition

func (TimestampColumn) Gte

func (c TimestampColumn) Gte(v any) Condition

func (TimestampColumn) IsNotNull

func (c TimestampColumn) IsNotNull() Condition

func (TimestampColumn) IsNull

func (c TimestampColumn) IsNull() Condition

func (TimestampColumn) Lt

func (c TimestampColumn) Lt(v any) Condition

func (TimestampColumn) Lte

func (c TimestampColumn) Lte(v any) Condition

type Transition added in v0.87.0

type Transition struct {
	// Key names the move in its route (POST <api>/<entity>/<id>/transitions/<key>)
	// and its MCP tool: lowercase segments joined by single underscores
	// (see transitionKeyGrammar), unique on the entity, and not one of the
	// names a generated surface already uses (reservedTransitionKeys).
	Key string `json:"key"`
	// Label is the button text. Empty draws the key.
	Label string `json:"label,omitempty"`
	// From lists the values the move starts from; never empty.
	From []string `json:"from"`
	// To is the value the move writes.
	To string `json:"to"`
	// Stamp names a Date or Timestamp field the move sets to the server's
	// current UTC date or time. The client never supplies it.
	Stamp string `json:"stamp,omitempty"`
	// Variant is the button variant screens draw the move with
	// (ui.ParseButtonVariant spellings). Empty is the screen's default.
	Variant string `json:"variant,omitempty"`
	// Permission, when set, is required on top of the entity's update
	// access. Empty means update access alone.
	Permission string `json:"permission,omitempty"`
	// System moves get no route, button or MCP tool; only Go code calls
	// RunTransition for them (a job marking invoices past due).
	System bool `json:"system,omitempty"`
}

Transition is one named move of the state field.

type UUIDColumn

type UUIDColumn struct {
	// contains filtered or unexported fields
}

UUIDColumn represents a UUID/text-shaped identity column.

func NewUUIDColumn

func NewUUIDColumn(name string) UUIDColumn

func (UUIDColumn) Asc

func (c UUIDColumn) Asc() Order

func (UUIDColumn) Desc

func (c UUIDColumn) Desc() Order

func (UUIDColumn) Eq

func (c UUIDColumn) Eq(v string) Condition

func (UUIDColumn) In

func (c UUIDColumn) In(values ...string) Condition

func (UUIDColumn) IsNotNull

func (c UUIDColumn) IsNotNull() Condition

func (UUIDColumn) IsNull

func (c UUIDColumn) IsNull() Condition

func (UUIDColumn) Neq

func (c UUIDColumn) Neq(v string) Condition

type ValidationRegistry

type ValidationRegistry struct {
	// contains filtered or unexported fields
}

ValidationRegistry holds a chain of validator functions.

func NewValidationRegistry

func NewValidationRegistry() *ValidationRegistry

NewValidationRegistry creates an empty ValidationRegistry.

func (*ValidationRegistry) RegisterValidator

func (vr *ValidationRegistry) RegisterValidator(fn ValidatorFunc)

RegisterValidator appends a validator function to the chain.

func (*ValidationRegistry) Validate

func (vr *ValidationRegistry) Validate(ctx context.Context, data map[string]any) map[string]string

Validate runs all registered validators and collects every field error. The returned map is field name → error message. A nil/empty map means valid.

func (*ValidationRegistry) Validators

func (vr *ValidationRegistry) Validators() int

Validators returns the number of registered validators (for testing).

type ValidatorFunc

type ValidatorFunc func(ctx context.Context, data map[string]any) map[string]string

ValidatorFunc validates entity data and returns field-level errors. The returned map is field name → error message (empty map means valid).

func Custom

func Custom(name string, fn func(ctx context.Context, data map[string]any) map[string]string) ValidatorFunc

Custom returns a validator with a given name that runs the provided function. The fn returns a map of field→error for any violations found.

func Required

func Required(fields ...string) ValidatorFunc

Required returns a validator that checks the given fields are present and non-zero.

func Unique

func Unique(field string, checkFn func(ctx context.Context, value any) bool) ValidatorFunc

Unique returns a validator that checks a field value is unique using the provided check function. The checkFn receives the field value and returns true if the value is unique (not taken).

type VersionedRegistry added in v0.48.0

type VersionedRegistry interface {
	// GetVersioned retrieves the entity registered under name at exactly
	// version (the route-group prefix it was mounted at; "" for the
	// unversioned App.Entity registration).
	GetVersioned(name, version string) (*Entity, error)
}

VersionedRegistry is the optional capability a registry advertises when it can resolve a specific version of an entity name.

Deliberately NOT part of Registry: that interface has a dozen implementations (test stubs, package-local fakes), and a registry with no concept of versions cannot hold two versions of a name, so Get is already unambiguous for it. Requiring the method would break every implementor to describe a capability most of them cannot exercise.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL