Entity Generator
vyuh_entity_generator reads @Entity, @Field, and @ServerEntity annotations from vyuh_entity_annotations and emits two kinds of part files:
*.entity.dart— client artifacts: base class, fields registry, API, layouts, form, metadata, routing, workspace config, and the assembledEntityConfiguration<T>.*.server.dart— server artifacts: base class,EntityCrudConfig<T>, and anEntityCrudDescriptor<T>ready for a server feature descriptor.
Two build_runner builders, twelve client emitters, and two server emitter groups. The server descriptor emitter writes both the CRUD config and the descriptor. Domain logic stays in your code; the generator only produces the generic infrastructure that would otherwise be repeated across every entity.
Installation
Authenticate with pub.vyuh.tech first
`vyuh_entity_generator` is hosted on the private pub.vyuh.tech registry. Before running pub get, register a Bearer token issued by Vyuh Technologies based on your plan.
Run the one-time setup:
dart pub token add https://pub.vyuh.techDon't have a token yet? Email ask@vyuh.tech to request one. For full details (CI, Docker, rotation, troubleshooting), see the Pub Token Setup guide.
dependencies:
vyuh_entity_annotations:
hosted: https://pub.vyuh.tech
version: ^3.3.0
vyuh_entity_system:
hosted: https://pub.vyuh.tech
version: ^5.1.0
dev_dependencies:
vyuh_entity_generator:
hosted: https://pub.vyuh.tech
version: ^3.3.0
build_runner: ^2.8.0
json_serializable: ^6.7.1The generator transitively pulls in source_gen, build, analyzer, code_builder, and dart_style.
One package, two roles
Add vyuh_entity_annotations everywhere you write models (client packages, server packages, shared packages). Add vyuh_entity_generator only to the packages where the generator should run — typically the *_entities packages on each side.
Builders
The generator ships two builders, both wired through build.yaml:
| Builder factory | Triggered by | Output extension | Runs before |
|---|---|---|---|
entityBuilder (package:vyuh_entity_generator/builder.dart) | @Entity | .entity.dart | json_serializable |
serverEntityBuilder (package:vyuh_entity_generator/server_builder.dart) | @ServerEntity | .server.dart | json_serializable |
Both run before json_serializable because the emitted code references fromJson/toJson helpers from the matching .g.dart file.
auto_apply: dependents means the builders activate automatically in any package that depends on vyuh_entity_generator — no extra build.yaml needed in your own package for the common case.
Running the generator
# One-shot
dart run build_runner build --delete-conflicting-outputs
# Watch mode
dart run build_runner watch --delete-conflicting-outputsAlways regenerate after annotation edits
Any change to @Entity, @Field, @ServerEntity, or @JsonSerializable requires re-running build_runner before considering the edit done. The generator does not run on save automatically.
analysis_options.yaml excludes **/*.entity.dart and **/*.server.dart from analysis, which means the analyzer cannot catch generator-emitted type errors. The canonical post-codegen verification path is hot-restart against the running app, or temporarily un-exclude the generated files in analysis_options.yaml.
Client emitters (.entity.dart)
EntityGenerator invokes twelve emitters in order and concatenates their output into a single *.entity.dart part file.
| Emitter | Output | Notes |
|---|---|---|
base_class_emitter -> emitBaseClass | $<Cls>Base superclass | Mixes in Auditable, Versionable, Draftable, and HasDescription based on capabilities and field declarations. |
fields_emitter -> emitFields | <Cls>Fields static-only class | One FieldDefinition<T> per @Field-annotated member, plus static const all = [...]. |
api_emitter -> emitApi | $<Cls>Api class extending HttpEntityApi<T> | Mixes in VersioningApiMixin<T>, DraftApiMixin<T>, RelationshipApiMixin<T> per capabilities; emits cachingPolicy override. |
table_emitter -> emitTable | $<cls>TableLayout() returning EntityTableConfig<T> | Returns null when EntityUILayouts.list is supplied (custom list layouts replace the generated table). |
kanban_emitter -> emitKanban | $<cls>KanbanLayout() returning EntityKanbanConfig<T> | Emitted when EntityUILayouts.kanban is supplied; columns come from enum options or explicit groups. |
detail_emitter -> emitDetail | $<Cls>DetailLayout class extending EntityLayout<T> | Skipped when EntityUILayouts.details already provides custom layouts. |
relationship_tab_emitter -> emitRelationshipTabs | One $<Cls><Suffix>RelationshipLayout per relationship | Wraps EntityRelationshipLayout<T, R>. Emits both manyToMany and oneToMany tabs from @Entity.relationships. |
metadata_emitter -> emitMetadata | $<cls>Metadata() returning EntityMetadata | Identity, icon, category/menuCategory, priority, visibility set, tags, alternate identifiers, default sort field, i18n. |
routing_emitter -> emitRouting | $<cls>Routing() returning EntityRouting<T> | path from EntityNavigation.prefix (default '/{schema}/{identifier}'), builder: EntityWorkspaceRouteBuilder<T>, permissions. |
form_emitter -> emitForm | $<cls>Form(...) returning vf.Form | Emitted only when the entity has an editor. Generated form covers all @Field declarations (see Form generation). |
config_emitter -> emitConfig | <cls>Config (final field of type EntityConfiguration<T>) | Wires metadata, api, fields, routing, layouts, editor, actions; resolves capability-derived defaults and Authorized wrappers. |
workspace_emitter -> emitWorkspace | $<cls>WorkspaceConfig() returning EntityWorkspaceConfig<T> | Wires the table layout into EntityBodyPart<T>. Authors copyWith to override header/detail parts. |
Example output
For a class class Area extends $AreaBase, the generator emits area.entity.dart along these lines:
part of 'area.dart';
// ─── Base class with capability mixins ───────────────────────────
abstract class $AreaBase extends EntityBase
with Auditable, Versionable, Draftable, HasDescription { ... }
// ─── Field registry ──────────────────────────────────────────────
class AreaFields {
static const code = FieldDefinition<String>(...);
static const name = FieldDefinition<String>(...);
static const capacity = FieldDefinition<int?>(...);
// ... one per @Field-annotated member
static const all = <FieldDefinition>[code, name, capacity, ...];
}
// ─── API base class ──────────────────────────────────────────────
class $AreaApi extends HttpEntityApi<Area>
with VersioningApiMixin<Area>, DraftApiMixin<Area>,
RelationshipApiMixin<Area> {
$AreaApi() : super(
entityType: 'areas',
schemaType: 'lms.areas',
pathBuilder: EndpointBuilder(prefix: 'areas'),
fromJson: Area.fromJson,
);
}
// ─── Default table layout ────────────────────────────────────────
EntityTableConfig<Area> $areaTableLayout() => EntityTableConfig<Area>(
title: 'Table', identifier: 'table', icon: FluentIcons.table_24_regular,
rowHeight: 64.0,
columns: [...],
);
// ─── Default detail layout ───────────────────────────────────────
class $AreaDetailLayout extends EntityLayout<Area> { ... }
// ─── Relationship tabs (one per @Entity.relationships entry) ────
class $AreaEquipmentsRelationshipLayout extends EntityLayout<Area> { ... }
// ─── Metadata, routing, form, workspace ──────────────────────────
EntityMetadata $areaMetadata() => EntityMetadata(...);
EntityRouting<Area> $areaRouting() => EntityRouting<Area>(...);
vf.Form $areaForm({String? entityId, bool isDraft = false, ...}) { ... }
EntityWorkspaceConfig<Area> $areaWorkspaceConfig() { ... }
// ─── The assembled config ────────────────────────────────────────
final areaConfig = EntityConfiguration<Area>(
metadata: $areaMetadata(),
api: $AreaApi(),
fields: AreaFields.all,
routing: $areaRouting(),
layouts: EntityLayouts(list: [$areaTableLayout()], ...),
editor: StandardEntityEditor<Area>(...),
actions: VersionableActions.defaults<Area>(),
);Generated identifier conventions
| Identifier | Pattern | Example |
|---|---|---|
| Base class | $<Cls>Base | $AreaBase |
| Fields registry | <Cls>Fields | AreaFields |
| API class | $<Cls>Api | $AreaApi |
| Effective API class | apiClass if set, otherwise $<Cls>Api | AreaApi (when apiClass: AreaApi) |
| Table layout function | $<cls>TableLayout() | $areaTableLayout() |
| Default detail layout | $<Cls>DetailLayout | $AreaDetailLayout |
| Relationship tab class | $<Cls><Suffix>RelationshipLayout | $AreaEquipmentsRelationshipLayout |
| Metadata function | $<cls>Metadata() | $areaMetadata() |
| Routing function | $<cls>Routing() | $areaRouting() |
| Form function | $<cls>Form({...}) | $areaForm({...}) |
| Workspace config function | $<cls>WorkspaceConfig() | $areaWorkspaceConfig() |
| Final config field | <cls>Config | areaConfig |
Form generation
form_emitter produces a real working form from @Field declarations. The emitter resolves a category for each field — FieldType annotation name > Dart type > structural keys (reference, enum) — and dispatches to the form-builder method registry:
| Category | Form builder method | Post-chain extras |
|---|---|---|
TextFieldType, String, EmailFieldType, UrlFieldType | .text(...) | EmailFieldType -> .email(); UrlFieldType -> .pattern(...) matching ^https?://. |
NumberFieldType, int, double, num | .number(...) | min, max, integerOnly. |
BooleanFieldType, bool | .boolean(...) | — |
DateTimeFieldType, DateTime | .dateTime(...) | — |
SelectFieldType, enum | .select(...) | .options(...) from options map; .multiple() when multi: true. |
PhoneFieldType | .phone(...) | — |
reference | .reference(...) | .provider(EntityReferenceProvider(...)), .allowCreate(), excludeEntityId: entityId for self. |
Common chainers (placeholder, minLength, maxLength, pattern) are applied to any field type from FieldType properties. @Field(unique: true) emits .unique(schema:, table:, fieldName:, excludeId:), and @Field(derivedFrom: 'name') emits .identifier().derivedFrom({'name'}, transformer: SnakeCaseTransform().apply, initiallyDirty: entityId != null). @Field(immutableAfterCreate: true) emits .enabled(entityId == null || isDraft).
The generated form signature is:
vf.Form $areaForm({
String? entityId,
bool isDraft = false,
Map<String, dynamic>? initialValues,
})Field definitions
fields_emitter resolves each @Field to a typed FieldDefinition<T>:
String->TextFieldDefint/double/num->NumberFieldDef(withallowDecimal: falseforintor whenintegerOnly: true)bool->BooleanFieldDefDateTime->DateTimeFieldDef- enum types ->
EnumFieldDef ReferenceFieldType(entityType: '...')->ReferenceFieldDef.auto(targetEntityType: '...')
Validation, search/sort flags, and uniqueness are forwarded into the FieldDefinition constructor.
Read-side relationship projections are declared through @Field(companion: FieldCompanion.lookup/sidecar/aggregate(...)). Lookup companions can expose dotted field paths (for example equipment_category.name) because FieldDefinition.getValue can walk nested JSON maps.
Server emitters (.server.dart)
ServerEntityGenerator invokes the base-class emitter and the descriptor emitter:
| Emitter | Output |
|---|---|
server_base_class_emitter -> emitServerBaseClass | $<Cls>Base superclass with capability mixins (Auditable, Versionable, Draftable) and their concrete stored fields. No Flutter dependencies. |
server_descriptor_emitter -> emitServerDescriptorAndCrudConfig | $<cls>ServerCrudConfig (EntityCrudConfig<T>) plus $<cls>ServerDescriptor (EntityCrudDescriptor<T>): schema/table, sortable/searchable/unique fields, hooks, embedded refs, companions, inverse embeds, virtual fields, projection table, OpenAPI component schema, scope, in-use guards, and field aliases. |
Example output
part of 'area.dart';
// ─── Base class ──────────────────────────────────────────────────
abstract class $AreaBase extends VersionedEntity
with Auditable, Versionable, Draftable { ... }
// ─── Server CRUD config ──────────────────────────────────────────
final $areaServerCrudConfig = EntityCrudConfig<Area>(
schema: 'lms',
table: 'areas',
searchableFields: ['code', 'description', 'room_number', 'name'],
sortableFields: ['code', 'parent_area_id', 'capacity', /* ... */, 'name'],
uniqueFields: ['name'], // auto-added unless uniqueName: false
defaultSortField: 'code',
fromJson: Area.fromJson,
toJson: (e) => e.toJson(),
entityName: 'Area',
entityNamePlural: 'Areas',
validateCreate: validateAreaCreate,
validateUpdate: validateAreaUpdate,
beforeDelete: checkAreasInUse,
openApiSchema: $AreaOpenApiSchema,
);
// ─── Server descriptor ───────────────────────────────────────────
final $areaServerDescriptor = EntityCrudDescriptor<Area>(
name: 'Areas',
basePath: '/areas',
config: $areaServerCrudConfig,
protectedPaths: null, // null => all paths protected
);Generated identifier conventions (server)
| Identifier | Pattern | Example |
|---|---|---|
| Base class | $<Cls>Base | $AreaBase |
| Server CRUD config | $<cls>ServerCrudConfig | $areaServerCrudConfig |
| Server descriptor | $<cls>ServerDescriptor | $areaServerDescriptor |
Add the descriptor to the server feature descriptor:
FeatureDescriptor(
name: 'lms',
descriptors: [$areaServerDescriptor],
);End-to-end flow
- Write the model. Annotate a class with
@Entity(client) or@ServerEntity(server), declarepart 'foo.entity.dart'(or'foo.server.dart') andpart 'foo.g.dart'. Annotate fields with@Field. - Run the generator.bash
dart run build_runner build --delete-conflicting-outputs - Verify. Hot-restart the app (analyzer cannot see generated files — they're excluded from
analysis_options.yaml), or temporarily un-exclude**/*.entity.dartto confirm zero analysis issues. - Register. Add the generated config to your
FeatureDescriptor(or add the server descriptor to your server feature):dart// Client final feature = FeatureDescriptor( name: 'feature_lms', extensions: [ EntityExtensionDescriptor(entities: [areaConfig, equipmentConfig]), ], ); // Server final lmsFeature = FeatureDescriptor( name: 'lms', descriptors: [$areaServerDescriptor, $equipmentServerDescriptor], );
Convention-based overrides (client)
The generator looks for hand-written hooks in the same library and uses them in place of the default. Lets you start with generator defaults and override just the piece you need without touching the annotation.
| Convention | Replaces / overrides |
|---|---|
class <Cls>Api extends $<Cls>Api | Use as apiClass: AreaApi to wire your subclass into the generated config. |
Function tear-off in @Entity(navigation: _myFn) | Replaces the generated EntityRouting<T>. |
Function tear-off in @Entity(layouts: _myFn) | Replaces the generated EntityLayouts<T>. |
@Entity(editor: MyEditor) (Type) or function tear-off | Replaces the auto-emitted StandardEntityEditor<T>. |
@Entity(actions: _myFn) | Replaces the generated EntityActions<T>. |
@Entity(layouts: EntityUILayouts(list: [...])) | Skips $<cls>TableLayout emission entirely. |
@Entity(layouts: EntityUILayouts(details: [...])) | Skips $<Cls>DetailLayout emission entirely. |
"Function tear-offs everywhere"
Every configuration slot supports function tear-offs because annotations must be const-expressible. The generator emits the call site as EntityRouting.function(_areaNavigation) (or the equivalent factory) so the framework knows to invoke the function at startup.
build.yaml configuration
The generator ships with sensible defaults via its own build.yaml. In most projects you do not need to add anything to your own build.yaml. If you need to constrain the input set, use the standard build_runner mechanism:
# build.yaml at your package root
targets:
$default:
builders:
vyuh_entity_generator|entity:
generate_for:
- lib/entities/**.dart
vyuh_entity_generator|server_entity:
generate_for:
- lib/src/**.dartThe generator's bundled build.yaml declares runs_before: ["json_serializable"], so the emitted EntityApi.fromJson and EntityConfig.fromJson references resolve correctly.
Scaffolding with vyuh_ems_cli
vyuh_ems_cli (binary vyuh_ems) scaffolds new EMS monorepos and adds features and entities. It writes the annotated source (@Entity, @JsonSerializable, etc.) and the matching pubspec.yaml/feature.dart wiring, then optionally invokes build_runner so the .entity.dart files are produced before you open the project.
vyuh_ems create my_app
vyuh_ems add feature inventory
vyuh_ems add entity Item --in inventoryThe CLI itself does not emit any generated code — that's still the generator's job, invoked under the hood (or skipped with --no-build-runner if you want to inspect the scaffolded source first).
Things you should never do
- Never edit
*.entity.dartor*.server.dartfiles — they are re-emitted on everybuild_runnerrun and your changes will vanish. Modify the annotations or write a hand-rolled override that the generator picks up via convention. - Never depend on
dart_styleorcode_builderfrom your application code — the generator uses them internally; consumers do not. - Never assume the analyzer caught a generator bug. The
analysis_options.yamlexclusion of**/*.entity.dart/**/*.server.dartmeans analysis errors in generated code are silent. Hot-restart, or temporarily un-exclude, after every annotation change.
Troubleshooting
Unresolved identifier _$AreaFromJson Run build_runner — json_serializable has not produced the .g.dart file yet. The generator's runs_before: ["json_serializable"] declaration ensures the generator emits first; running build_runner build produces both files in one pass.
@Entity annotation not recognized Ensure you import 'package:vyuh_entity_annotations/vyuh_entity_annotations.dart'; in the annotated file, and declare both part 'area.entity.dart'; and part 'area.g.dart'; near the top of the same file.
Generated form is missing fields The generated form covers every @Field-annotated member in declaration order. If a property is missing, check that it carries @Field(label: ...) — a bare final String foo; is invisible to the generator.
Changes to @Entity arguments do not appear after rebuild Some changes (new capabilities, new relationships) require deleting the output file so build_runner regenerates from scratch:
dart run build_runner build --delete-conflicting-outputsAnalyzer still reports zero errors after a known-bad annotation change The repo's analysis_options.yaml excludes **/*.entity.dart and **/*.server.dart. Either hot-restart the running app to surface runtime errors, or temporarily remove those exclusions and re-run the analyzer.
What's next?
- Entity Annotations — full reference for
@Entity,@Field,@ServerEntity, capabilities, layouts, and authorization. - Defining Entities — end-to-end walkthrough that pairs the generator with the runtime.
- Entity Configuration API — reference for the generated
EntityConfiguration<T>shape. - Configuration concepts — how the generated config maps onto runtime behaviour.