Skip to content

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 omit name from 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.

MixinAddsNotes
AuditablecreatedAt, updatedAt, createdBy, updatedByTypically server-set.
VersionableversionNumber, isActiveRequired before Draftable.
DraftabledraftMetadata plus draft-status gettersRequires 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 to category.schema + identifier when 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:

dart
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).
  • operationWorkflowOperation (registration, modification, activation, deactivation, restoration, unknown).
  • statusDraftStatus (draft, underReview, approved, effective, rejected, cancelled, deleted, failed, plus transient submitted and revision-loop revisionRequested).
  • Audit-style attribution (createdBy, updatedBy, timestamps).
  • Optional workflow link (workflowInstanceId), revision feedback (revisionComments, revisionRequestedBy, revisionRequestedAt), and an canEditDraft flag 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:

FieldPurpose
identifierRegistration key + permission namespace.
name, pluralName, descriptionStatic display strings (with *Resolver overloads for i18n).
categoryEntityCategory for top-level navigation grouping.
visibilitySet<UIVisibility> — see below.
isSingletonTrue when only one instance exists.
prioritySort order in menus (lower wins; default 999).
schemaTypeOptional schema-qualified type name.
alternateIdentifiersExtra keys for cross-context lookup.
subtitleBuilderPer-instance subtitle for picker / reference views.
getHelpPulls a contextual help bundle.

Visibility

UIVisibility is a sealed class declaring which surfaces show this entity. Set membership is the API:

VariantEffect
RouteVisibilityThe entity gets GoRoutes.
MenuVisibility(menuCategory:, sortOrder:)Appears in the navigation menu (implies route).
SearchVisibility(additionalSearchTerms:)Discoverable in the command palette.
DashboardVisibilitySurfaces 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.