registry

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 22, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

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

View Source
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.

View Source
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

View Source
var ByID = Scope{Name: "id"}

ByID scopes deltas to a single aggregate: changes:{tenant}:id:{aggID}.

View Source
var ByTenant = Scope{Name: "tenant"}

ByTenant scopes deltas to everything in the tenant.

Functions

func ChannelName

func ChannelName(tenantID string, scope Scope, id string) string

ChannelName derives a subscription channel. Channels are tenant-prefixed and only ever constructed here: changes:{tenant}:{scope}:{id}.

func DocChannelName

func DocChannelName(tenantID, docID string) string

DocChannelName derives the RAW document-sync channel for one document: doc:{tenant}:{docID}. Frames on it are never conflated.

func EventType

func EventType(entity, verb string) string

EventType derives the canonical event type for an entity and verb, e.g. "asset.updated".

func GraphName

func GraphName(tenantID string) string

GraphName derives the per-tenant graph name (FalkorDB key).

func GraphNameVersioned

func GraphNameVersioned(tenantID string, version int) string

GraphNameVersioned derives a blue-green build target for rebuilds; the live pointer is tracked in projection_state.

func SearchIndexAlias

func SearchIndexAlias(tenantID, base string) string

SearchIndexAlias derives the stable per-tenant alias for a search index; reads and writes go through the alias, rebuilds swap it atomically.

func SearchIndexVersioned

func SearchIndexVersioned(tenantID, base string, version int) string

SearchIndexVersioned derives the concrete versioned index behind the alias.

func StreamKey

func StreamKey() string

StreamKey is the single Redis stream all domain events are relayed to; projections consume it through their own consumer groups.

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) HasColumn

func (b *Binding) HasColumn(col string) bool

HasColumn reports whether the bound table has the given column.

func (*Binding) IsDynamic

func (b *Binding) IsDynamic() bool

IsDynamic reports whether this binding describes a runtime-defined entity (declared via DynamicSchema rather than a Go Model).

func (*Binding) ModelType

func (b *Binding) ModelType() reflect.Type

ModelType returns the bound struct type (not pointer).

func (*Binding) NewModel

func (b *Binding) NewModel() any

NewModel returns a pointer to a fresh zero value of the bound model type.

func (*Binding) Populate

func (b *Binding) Populate(model any, vals map[string]any) error

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

func (b *Binding) Required() []string

Required returns the non-structural columns that must be provided on create/update: NOT NULL, no default, not auto-generated.

func (*Binding) ValuesByColumn

func (b *Binding) ValuesByColumn(model any) (map[string]any, error)

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
}

CRDTSpec configures the document plane for KindDocument entities. The merge engine comes from grove's crdt packages — referenced, not reimplemented.

type CacheSpec

type CacheSpec struct {
	TTL    time.Duration
	Scoped bool
}

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
)

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

type DynamicIndex struct {
	Name    string
	Columns []string
	Unique  bool
}

DynamicIndex is an optional secondary index on a dynamic entity.

type DynamicSchema

type DynamicSchema struct {
	Table   string
	Columns []DynamicColumn
	Indexes []DynamicIndex
}

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

type EmbedSpec struct {
	Fields []string
	Text   func(vals map[string]any) string
}

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
)

func (Kind) String

func (k Kind) String() string

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 New

func New() *Registry

New returns an empty registry.

func (*Registry) All

func (r *Registry) All() []*Entity

All returns every registered entity, sorted by name for determinism.

func (*Registry) Get

func (r *Registry) Get(name string) (*Entity, bool)

Get returns the entity registered under name.

func (*Registry) GetByModelType

func (r *Registry) GetByModelType(t reflect.Type) (*Entity, bool)

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) Validate

func (r *Registry) Validate() error

Validate performs startup validation across entities: every edge target must itself be registered. Call once after all Register calls.

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.

func ByField

func ByField(name, field string) Scope

ByField declares a containing scope whose channel id comes from the named column, e.g. ByField("site", "site_id").

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.

Jump to

Keyboard shortcuts

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