Entity Model
Every business object handled by the entity system is an entity — a typed Dart class that extends EntityBase and opts into capabilities via mixins. The model is intentionally small: the base only carries identity, and every other concern (audit, versioning, drafts, custom display) is layered on by explicit composition.
EntityBase
EntityBase extends ContentItem from vyuh_core, so every entity participates in the framework's content pipeline (it gets schemaType, layout, and modifiers). The base only adds identity and a small display contract:
id— globally unique identifier (required).name— primary display string. Master-data entities should keep names unique per type so operators don't confuse two equipments called "Autoclave"; instance entities that allow duplicates omitnamefrom their editor and rely on the DB constraint.displayTitle,displaySubtitle,displayProperties— overridable hooks used by reference cards, mobile compact rows, and search results.toJson()— required on every concrete entity (typically generated).
Every other capability (audit timestamps, version numbers, draft metadata) is not on the base. It is added by mixing in Auditable, Versionable, or Draftable. The code generator emits a $EntityBase superclass that mixes those in for you with the right @JsonKey configuration; for hand-written entities you mix them in yourself.
See EntityBase API for the field-by-field signature.
Capability mixins
The mixins compose in a fixed order — each layer assumes the previous one is present.
| Mixin | Adds | Notes |
|---|---|---|
Auditable | createdAt, updatedAt, createdBy, updatedBy | Typically server-set. |
Versionable | versionNumber, isActive | Required before Draftable. |
Draftable | draftMetadata plus draft-status getters | Requires Versionable. |
Property mixins
Domain-shape mixins (HasCode, HasDescription, HasActiveStatus, HasStatus, and the composite CommonEntityProperties) advertise that an entity exposes a particular field. Framework-side code reads these via EntityPropertyAccess extensions (entity.codeIfSupported, entity.descriptionIfSupported, entity.statusIfSupported, entity.isActiveIfSupported) and via entity.isDraft for the draft check. Use them to keep helpers type-safe without resorting to dynamic JSON probing.
Schema-type identifier
Two related identifiers describe an entity type:
EntityMetadata.identifier— the logical name used to register the entity, look up configs, and check permissions (e.g.'courses','equipment').EntityMetadata.schemaType— the schema-qualified type name used for serialization and API routing (e.g.'lms.course','lms.equipment'). Falls back tocategory.schema + identifierwhen null.
Some screens index entities under multiple keys (DB stores singular, identifier is plural). Add the alternate keys to alternateIdentifiers so name lookups resolve in either form.
EntityReference
EntityReference is the lightweight handle for cross-entity relationships. Storing a reference instead of the full related entity keeps payloads small and avoids cascading fetches. References compare equal by id only, so collection ops (contains, dedup) work even when the cached display name shifts.
Use references in entity fields:
class Course extends EntityBase with Auditable {
final EntityReference? instructor;
// ...
@override
Map<String, String> get displayProperties => {
if (instructor != null) 'Instructor': instructor!.name,
};
}The framework also keeps an in-memory EntityNameCacheService so that lists of references render names without per-row API calls.
Drafts and versioning
Entities that support an approval workflow mix in Versionable and Draftable. The DraftMetadata payload is populated by the server on every read and stripped from toJson() (it is includeToJson: false). It carries:
draftId— primary key of the draft row (different from the entity id).operation—WorkflowOperation(registration,modification,activation,deactivation,restoration,unknown).status—DraftStatus(draft,underReview,approved,effective,rejected,cancelled,deleted,failed, plus transientsubmittedand revision-looprevisionRequested).- Audit-style attribution (
createdBy,updatedBy, timestamps). - Optional workflow link (
workflowInstanceId), revision feedback (revisionComments,revisionRequestedBy,revisionRequestedAt), and ancanEditDraftflag computed by the server.
Draftable exposes visual/state helpers (isEntityDraft, hasModificationDraft, showDraftIndicator). Draft API identity is stricter: entity.isDraftRecord / entity.isDraft is true only when the row itself is from the drafts partition (entity.id == draftMetadata.draftId). An approved row can still carry draftMetadata when it has a pending modification draft; that metadata should show a badge, but the row should still be routed through the approved entity id.
EntityVersion and EntityAudit (in versioning_audit/) describe the historical records the audit and version-history tabs render. Both carry the acting user id; the EntityNameCacheService resolves the display name on the client.
For full lifecycle treatment see the drafts & versioning guide.
EntityMetadata
EntityMetadata is purely descriptive — what the entity is, where it shows up, which icon and names it uses. Behavioural configuration (routing, actions, layouts) lives in EntityConfiguration.
Key fields:
| Field | Purpose |
|---|---|
identifier | Registration key + permission namespace. |
name, pluralName, description | Static display strings (with *Resolver overloads for i18n). |
category | EntityCategory for top-level navigation grouping. |
visibility | Set<UIVisibility> — see below. |
isSingleton | True when only one instance exists. |
priority | Sort order in menus (lower wins; default 999). |
schemaType | Optional schema-qualified type name. |
alternateIdentifiers | Extra keys for cross-context lookup. |
subtitleBuilder | Per-instance subtitle for picker / reference views. |
getHelp | Pulls a contextual help bundle. |
Visibility
UIVisibility is a sealed class declaring which surfaces show this entity. Set membership is the API:
| Variant | Effect |
|---|---|
RouteVisibility | The entity gets GoRoutes. |
MenuVisibility(menuCategory:, sortOrder:) | Appears in the navigation menu (implies route). |
SearchVisibility(additionalSearchTerms:) | Discoverable in the command palette. |
DashboardVisibility | Surfaces in dashboard widgets. |
Convenience: UIVisibility.all(menuCategory: ...) returns the standard "everywhere" set. Pass const {} to opt the entity out of every surface (junction tables, embedded sub-entities) — the workspace shell can still mount it via getConfig<T>().buildRoutes().
EntityCategory
EntityCategory groups entities for menu organisation. It carries an id, display name, optional icon, sortOrder, and schema (defaults to the id). Categories support the same *Resolver localisation pattern as metadata.
Field definitions
Field definitions describe a single column / form field once and let layouts, forms, filters, and tables refer to it by reference instead of by string. This gives compile-time safety: a column can only render a field that the entity actually exposes.
The base is FieldDefinition<T extends EntityBase>, and the typed subclasses are TextFieldDef, NumberFieldDef, EnumFieldDef, BooleanFieldDef, DateFieldDef, DateTimeFieldDef, and ReferenceFieldDef. Common properties include field, label, required, filterable, sortable, primary, secondary, priority (uses ColumnPriority constants), icon, and showInCard.
Field definitions are data, not UI — width and minWidth belong on table columns, not on the field itself. Tables consume field definitions via EntityTableColumn(fieldDef: ...). See the layouts page for how columns assemble.
Next steps
- Configuration — composing fields, layouts, actions, and routing into an
EntityConfiguration<T>. - Architecture — how registration and the plugin lifecycle work.
- Defining Entities — annotating an entity end-to-end.