Documentation
¶
Overview ¶
Package registry is fabriq's declarative schema registry. Entities are described once as EntitySpecs — relational shape via a grove-tagged model, fabric-only concerns (graph mapping, search mapping, subscription scopes, CRDT plane) layered on top — and everything else is derived: projection mappings, channel names, tenant-scoped store names, conformance checks.
The registry never generates DDL; grove migrations remain the schema authority and the registry-conformance test is the bridge.
Index ¶
- Constants
- Variables
- func ChannelName(tenantID string, scope Scope, id string) string
- func CoerceRow(ent *Entity, vals map[string]any) error
- func CoerceToColumn(t ColumnType, v any) (any, error)
- func DocChannelName(tenantID, docID string) string
- func DocPresenceChannelName(tenantID, docID string) string
- func EventType(entity, verb string) string
- func GraphName(tenantID string) string
- func GraphNameVersioned(tenantID string, version int) string
- func IsReservedColumn(name string) bool
- func SearchIndexAlias(tenantID, base string) string
- func SearchIndexVersioned(tenantID, base string, version int) string
- func StreamKey() string
- type Binding
- func (b *Binding) DynColumn(col string) (DynamicColumn, bool)
- func (b *Binding) HasColumn(col string) bool
- func (b *Binding) IsDynamic() bool
- func (b *Binding) ModelType() reflect.Type
- func (b *Binding) NewModel() any
- func (b *Binding) Populate(model any, vals map[string]any) error
- func (b *Binding) Required() []string
- func (b *Binding) ValuesByColumn(model any) (map[string]any, error)
- type CRDTSpec
- type CacheSpec
- type ColumnType
- type DistillSpec
- type DynamicColumn
- type DynamicIndex
- type DynamicSchema
- type EdgeSpec
- type EmbedSpec
- type Entity
- type EntitySpec
- type GraphEdgeSpec
- type Kind
- type LiveSpec
- type Registry
- func (r *Registry) All() []*Entity
- func (r *Registry) Get(name string) (*Entity, bool)
- func (r *Registry) GetByModelType(t reflect.Type) (*Entity, bool)
- func (r *Registry) MustRegister(spec EntitySpec)
- func (r *Registry) Register(spec EntitySpec) error
- func (r *Registry) Replace(spec EntitySpec) error
- func (r *Registry) Unregister(name string) error
- func (r *Registry) Validate() error
- type Scope
- type SearchSpec
Constants ¶
const ( VerbCreated = "created" VerbUpdated = "updated" VerbDeleted = "deleted" )
Event verbs. EventType(entity, verb) is the only way event type strings are minted, so appliers and consumers can rely on the shape.
const ( ColumnID = "id" ColumnTenant = "tenant_id" ColumnVersion = "version" // ColumnScope is the optional secondary-scope column. It is nullable: a NULL // scope_id means the row is "shared" and visible in all scoped and unscoped // reads within the tenant. A non-NULL value restricts the row to that scope // (plus unscoped reads). Consumers that want secondary scoping must declare // this column in their entity model (grove tag: `db:"scope_id"`) and apply // migrations.ScopeAwareTenantPolicy to the table. ColumnScope = "scope_id" )
Structural column names used by fabriq adapters.
ColumnID, ColumnTenant, and ColumnVersion are REQUIRED on every fabriq-managed entity table: they make tenancy (RLS) and optimistic concurrency enforceable by construction.
ColumnScope is OPTIONAL. When a table carries it, consumers may partition rows within a tenant into a secondary scope (e.g. "project" within a "workspace") by adding the column and applying migrations.ScopeAwareTenantPolicy. Fabriq stamped-write paths detect presence of the column and fill it from the context scope; read paths enforce the soft filter automatically.
Variables ¶
var ByID = Scope{Name: "id"}
ByID scopes deltas to a single aggregate: changes:{tenant}:id:{aggID}.
var ByTenant = Scope{Name: "tenant"}
ByTenant scopes deltas to everything in the tenant.
Functions ¶
func ChannelName ¶
ChannelName derives a subscription channel. Channels are tenant-prefixed and only ever constructed here: changes:{tenant}:{scope}:{id}.
func CoerceRow ¶ added in v0.0.4
CoerceRow coerces and type-checks a dynamic entity's column-keyed values in place against their declared ColumnTypes, replacing each value with its canonical Go type. It is a no-op for Go-model entities (their struct fields are already typed) and skips the structural columns id/tenant_id/version, which are stamped by the executor / materializer rather than typed by the caller. A returned error names the offending entity and column.
func CoerceToColumn ¶ added in v0.0.4
func CoerceToColumn(t ColumnType, v any) (any, error)
CoerceToColumn validates v against the declared column type t and returns the canonical Go value for that type. A nil v passes through unchanged — column nullability is enforced separately by the required-field check. It returns an error when v cannot represent t.
Coercion is lenient where it is safe and lossless: JSON numbers decode to float64, so ColInt accepts any integral float and ColFloat accepts any integer; ColTime accepts an RFC3339 string. ColJSON accepts any value.
func DocChannelName ¶
DocChannelName derives the RAW document-sync channel for one document: doc:{tenant}:{docID}. Frames on it are never conflated.
func DocPresenceChannelName ¶ added in v0.0.5
DocPresenceChannelName derives the RAW awareness channel for one document: docpresence:{tenant}:{docID}. Presence frames are ephemeral — capped stream, tailed from now, never persisted, no delivery guarantees.
func EventType ¶
EventType derives the canonical event type for an entity and verb, e.g. "asset.updated".
func GraphNameVersioned ¶
GraphNameVersioned derives a blue-green build target for rebuilds; the live pointer is tracked in projection_state.
func IsReservedColumn ¶ added in v0.0.4
IsReservedColumn reports whether name is a structural column fabriq manages automatically (id, tenant_id, version). Entity specs must not declare a domain column with a reserved name — fabriq provides and stamps these. Note scope_id is NOT reserved: it is an opt-in column consumers declare themselves.
func SearchIndexAlias ¶
SearchIndexAlias derives the stable per-tenant alias for a search index; reads and writes go through the alias, rebuilds swap it atomically.
func SearchIndexVersioned ¶
SearchIndexVersioned derives the concrete versioned index behind the alias.
Types ¶
type Binding ¶
type Binding struct {
Table string
Columns []string
PK string
TenantColumn string
VersionColumn string
// contains filtered or unexported fields
}
Binding is the compiled relational shape of an entity, derived from its grove-tagged model at registration time, or from a DynamicSchema.
func (*Binding) DynColumn ¶ added in v0.0.4
func (b *Binding) DynColumn(col string) (DynamicColumn, bool)
DynColumn returns the declared dynamic column for col. ok is false when this binding is not dynamic or col is not one of its declared domain columns.
func (*Binding) IsDynamic ¶
IsDynamic reports whether this binding describes a runtime-defined entity (declared via DynamicSchema rather than a Go Model).
func (*Binding) NewModel ¶
NewModel returns a pointer to a fresh zero value of the bound model type.
func (*Binding) Populate ¶
Populate sets the model's fields from column-keyed values (the reverse of ValuesByColumn). Numeric JSON widening (float64 -> int64 etc.) is converted; incompatible types error.
func (*Binding) Required ¶
Required returns the non-structural columns that must be provided on create/update: NOT NULL, no default, not auto-generated.
func (*Binding) ValuesByColumn ¶
ValuesByColumn extracts the model's field values keyed by column name. This is the canonical payload shape: event payloads, graph node props and search documents are all column-keyed. For dynamic entities model must be a map[string]any; unknown keys are dropped.
type CRDTSpec ¶
type CRDTSpec struct {
Engine string // engine reference, e.g. "grove-crdt"
SnapshotEvery int // compact after this many updates
QuietWindow time.Duration // idle window before materialization
// ArchiveHistory opts this entity's documents into offloading sealed
// update history to the blob plane on Compact (latest state stays in the
// DB). nil = inherit the global Config.Documents.ArchiveHistory default;
// a non-nil pointer overrides it per entity.
ArchiveHistory *bool
}
CRDTSpec configures the document plane for KindDocument entities. The merge engine comes from grove's crdt packages — referenced, not reimplemented.
type CacheSpec ¶
CacheSpec opts an entity into the read-through row cache (P3). Nil (the zero value on EntitySpec) means caching is disabled for the entity. Scoped picks the cache partition: true => tenant+scope, false => tenant. TTL bounds each cached row (0 = no expiry; per-id eviction on write still applies).
type ColumnType ¶
type ColumnType int
ColumnType is the neutral column type set for dynamic entities; adapters map it to engine SQL types.
const ( ColText ColumnType = iota ColInt ColFloat ColBool ColTime ColJSON )
func (ColumnType) String ¶ added in v0.0.4
func (t ColumnType) String() string
String renders the column type name used in validation error messages.
type DistillSpec ¶
type DistillSpec struct {
SourceFields []string
Text func(vals map[string]any) string
Scopes []string
Budget int
}
DistillSpec opts an entity into context distillation. Declarative metadata only — the distillation layer supplies the summarization model and the guard. SourceFields names the columns concatenated into the L0 source text; Text, when set, overrides SourceFields. Scopes names the declared scope names that form L1 backbone digest nodes. Budget is the L0 summary token budget (0 = config default).
type DynamicColumn ¶
type DynamicColumn struct {
Name string
Type ColumnType
NotNull bool
// Default is an optional SQL default EXPRESSION (e.g. "now()", "'pending'",
// "0"). It is interpolated verbatim into DDL and is intentionally NOT
// identifier-validated (it is an expression, not an identifier), so it must
// be a trusted, control-plane value — never a user-supplied string. Same
// trust level as hand-written migration SQL.
Default string
}
DynamicColumn is one domain column of a runtime-defined entity.
type DynamicIndex ¶
DynamicIndex is an optional secondary index on a dynamic entity.
type DynamicSchema ¶
type DynamicSchema struct {
Table string
Columns []DynamicColumn
Indexes []DynamicIndex
// NoTypeCheck opts the entire entity out of application-level payload type
// validation on the command plane. Column values are then passed through
// unchanged and only the database column types enforce shape. Zero value
// (false) = validate.
NoTypeCheck bool
}
DynamicSchema describes an entity defined at runtime instead of by a Go Model. Mutually exclusive with EntitySpec.Model. fabriq injects the structural columns (id, tenant_id, version); declare only domain columns.
type EdgeSpec ¶
type EdgeSpec struct {
Field string // FK column on this entity's table
Rel string // relationship type, e.g. "LOCATED_AT"
Target string // registry name of the target entity
}
EdgeSpec maps a foreign-key column to a graph relationship.
type EmbedSpec ¶
EmbedSpec opts an entity into vector embedding. It is declarative metadata only — the agent layer supplies the embedding model. Fields names the columns whose values are concatenated into the embed text; Text, when set, overrides Fields and builds the text from column values.
type Entity ¶
type Entity struct {
Spec EntitySpec
Binding *Binding
}
Entity is a registered, compiled spec: the declarative EntitySpec plus its relational Binding.
type EntitySpec ¶
type EntitySpec struct {
Name string
Kind Kind
Model any
GraphNode string // graph label; empty = not projected to the graph
GraphEdge *GraphEdgeSpec // when set, the entity projects as a relationship
Edges []EdgeSpec
Search SearchSpec
Subscribe []Scope
CRDT *CRDTSpec
// Schema declares a runtime-defined ("dynamic") entity instead of Model.
// Exactly one of Model or Schema must be set.
Schema *DynamicSchema
// Validate, when set, runs after structural validation on every
// create/update/upsert with the column-keyed payload. Fabriq attaches
// no meaning to the values; consumers enforce their own invariants
// (enum membership, checksums, cross-field rules).
Validate func(vals map[string]any) error
// Live opts the entity into the maintained-result-set live query engine.
// Nil (the zero value) means live queries are disabled for this entity.
Live *LiveSpec
// Cache opts the entity into the read-through row cache. Nil = not cached.
Cache *CacheSpec
// Embed opts the entity into vector embedding (auto-indexing). Nil = not embedded.
Embed *EmbedSpec
// Distill opts the entity into context distillation: each row gets an
// L0 digest summary; declared Scopes form L1 backbone nodes. Nil = not
// distilled. The distillation layer supplies the Summarizer/Guard.
Distill *DistillSpec
}
EntitySpec declares one entity. Model must be a grove-tagged struct pointer such as (*domain.Asset)(nil); its table and columns are bound at registration.
type GraphEdgeSpec ¶
type GraphEdgeSpec struct {
TypeField string
SourceField string
TargetField string
SourceLabel string
TargetLabel string
PropFields []string
}
GraphEdgeSpec maps a reified-edge ENTITY (rows that ARE relationships) into the graph. Endpoints are matched by id under their identity labels; the rel type comes from a column value. General: reified relationships (membership, grant, subscription) are a common pattern, not specific to any domain.
type Kind ¶
type Kind int
Kind classifies how an entity is written.
const ( // KindAggregate entities are written exclusively through the command // plane: one transactional write, one versioned outbox event. KindAggregate Kind = iota // KindDocument entities are collaborative CRDT documents: updates land // in the append-only document plane and are periodically materialized // into an ordinary versioned domain event. The plane's implementation // is deferred; the seam exists from phase 1. KindDocument )
type LiveSpec ¶
type LiveSpec struct {
Filterable []string // columns allowed in Where (empty = all)
Sortable []string // columns allowed in Sort (empty = all)
MaxWindow int // cap on Limit (0 = engine default)
}
LiveSpec opts an entity into the live query engine (nil = disabled). Filterable/Sortable default to all columns when empty; columns are validated against the model at registration.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds all registered entities. Registration happens at startup; lookups are concurrent and read-only afterwards.
func (*Registry) GetByModelType ¶
GetByModelType returns the entity bound to the given model struct type; it powers hydration-target inference in TraverseAndHydrate.
func (*Registry) MustRegister ¶
func (r *Registry) MustRegister(spec EntitySpec)
MustRegister is Register that panics; for static wiring in domain packs.
func (*Registry) Register ¶
func (r *Registry) Register(spec EntitySpec) error
Register compiles and validates a spec. Cross-entity references (edge targets) are checked in Validate once all entities are registered.
func (*Registry) Replace ¶ added in v0.0.4
func (r *Registry) Replace(spec EntitySpec) error
Replace re-registers an existing DYNAMIC entity with an updated spec, recomputing its Binding. It is the runtime-mutation counterpart to Register (which rejects duplicates). Replacing an unknown entity, or a modelled (static) entity, is an error — statics are compile-time and keep the "register before Open" contract.
func (*Registry) Unregister ¶ added in v0.0.4
Unregister removes a DYNAMIC entity from the registry. Removing an unknown or modelled entity is an error.
type Scope ¶
type Scope struct {
// Name appears in channel names: changes:{tenant}:{name}:{id}.
Name string
// Field is the model column whose value provides the channel id for
// containing-scope channels (e.g. "site_id" for a by-site scope).
// Empty for the ByID and ByTenant builtins.
Field string
}
Scope names a subscription dimension. Channels are always resolved server-side from (tenant, scope, id) — clients never name channels.
type SearchSpec ¶
type SearchSpec struct {
Index string // logical index base name; tenant routing is derived
Fields []string // columns included in the indexed document
}
SearchSpec maps an entity into the search projection. The zero value (empty Index) means the entity is not indexed.