dorm_generator 2.0.0-dev.1 copy "dorm_generator: ^2.0.0-dev.1" to clipboard
dorm_generator: ^2.0.0-dev.1 copied to clipboard

Generates code to implement the dORM framework based on annotations.

dorm_generator #

pub package pub popularity pub likes pub points

Provides code adapted to work with the dORM framework.

Getting started #

Run the following commands inside your project:

dart pub add dev:dorm_generator
dart pub add dev:build_runner
dart pub get

Usage #

Note: This document assumes that you have already seen the dorm_annotations documentation.

Generating #

Create a file inside the lib folder of your Dart project. In this example, it will be lib/models.dart.

Write the classes, their getters and their annotations to this file. Add the following directives to top of this file:

import 'package:dorm_annotations/dorm_annotations.dart';
import 'package:dorm_framework/dorm_framework.dart';

part 'models.g.dart';

part 'models.dorm.dart';

Run the following line in your command prompt:

dart run build_runner build

This will generate all the files based on the annotated classes.

Models #

Any class _Class annotated with Model will create four new classes: ClassData, Class, ClassDependency and ClassEntity.

@Model(name: 'class', as: #classes)
abstract class _Class {
  @Field(name: 'name')
  String? get name;

  @Field(name: 'timestamp')
  DateTime get timestamp;

  @ForeignField(name: 'school-id', referTo: _School)
  String get schoolId;
}

Data

A ClassData will contain only the getters annotated with Field, PolymorphicField and ModelField. In the above example is defined as:

@JsonSerializable(anyMap: true, explicitToJson: true)
class ClassData {
  @JsonKey(name: 'name')
  final String? name;

  @JsonKey(name: 'timestamp', required: true, disallowNullValue: true)
  final DateTime timestamp;

  factory ClassData.fromJson(Map json) => _$ClassDataFromJson(json);

  const ClassData({
    required this.name,
    required this.timestamp,
  });

Map<String, Object?> toJson() => _$ClassDataToJson(this);
}

Derived fields

DerivedField is declared on a static method named $dorm$derived$<fieldName>. The generator emits a getter with that suffix and calls the method with the generated model and DerivedTransformations. The callback result is added to the generated model's serialized representation.

Model

A Class extends ClassData, implements _Class, has an additional id field and will contain only the getters annotated with ForeignField and DerivedField. In the above example is defined as:

@JsonSerializable(anyMap: true, explicitToJson: true)
class Class extends ClassData implements _Class {
  @JsonKey(name: '_id', required: true, disallowNullValue: true)
  final String id;

  @JsonKey(name: 'school-id', required: true, disallowNullValue: true)
  final String schoolId;

  factory Class.fromJson(String id, Map json) =>
      _$ClassFromJson({...json, '_id': id});

  const Class({
    required this.id,
    required this.schoolId,
    required super.name,
    required super.timestamp,
  });

  Map<String, Object?> toJson() =>
      _$ClassToJson(this)
        ..remove('_id');
}

dORM components

A ClassDependency and a ClassEntity extends respectively Dependency<ClassData> and Entity<ClassData, Class>, exported by the dorm_framework package.

Accessors

The code generation will also create a new class named Dorm, which will contain the repository accessors. In the above example, it is defined as:

class Dorm {
  final BaseEngine _engine;

  const Dorm(this._engine);

  DatabaseEntity<ClassData, Class> get classes =>
      DatabaseEntity(const ClassEntity(), engine: _engine);
}

Refer to the dorm_*_database packages to read more about how to obtain a Engine. With a Dorm instance, you can operate on classes using a Repository, also exported by dorm_framework:

void main() async {
  final Dorm dorm /* =  ... */;

  // Create
  final Class c = await dorm.classes.repository.put(
    Creation.auto(
      dependency: ClassDependency(schoolId: 'school-0'),
      data: ClassData(name: 'A class.', timestamp: DateTime.now()),
    ),
  );

  // Read
  final Future<Class> fc = await dorm.classes.repository.peek('class-1');
  final Future<List<Class>> fcs = await dorm.classes.repository.peekAll();
  final Stream<Class> sc = dorm.classes.repository.pull('class-1');
  final Stream<List<Class>> scs = dorm.classes.repository.pullAll();

  // Update
  final Class uc = await dorm.classes.repository.push(Class(
    id: 'class-1',
    schoolId: 'school-1',
    name: 'A new class.',
    timestamp: DateTime.now(),
  ));

  // Delete
  await dorm.classes.repository.pop('class-1');
}

Polymorphism #

Consider the following annotated code:

abstract class _Action {}

@PolymorphicData(name: 'attack')
abstract class _Attack implements _Action {
  @Field(name: 'strength')
  int get strength;
}

@PolymorphicData(name: 'defence')
abstract class _Defense implements _Action {
  @Field(name: 'resistence')
  int get resistence;
}

@PolymorphicData(name: 'healing', as: #heal)
abstract class _Healing implements _Action {
  @Field(name: 'health')
  int get health;
}

@Model(name: 'operation', as: #operations)
abstract class _Operation {
  @Field(name: 'name')
  String get name;

  @PolymorphicField(name: 'action', pivotName: 'type', pivotAs: #type)
  _Action get action;
}

The generated code will contain an abstract class named Action with three subclasses: Attack, Defense and Healing. It'll also contain an enum named ActionType with three values: attack, defense and heal (not healing; see its PolymorphicData's as argument).

The _Operation model will be generated as described previously, except that will contain an additional field named type of type ActionType, which will allow the user to check the runtime type of the action field.

The following code explains how to manipulate generated code for a Model with a field annotated with PolymorphicField:

void main() async {
  final Operation o1 = await dorm.operations.repository.put(
    Creation.auto(
      dependency: const OperationDependency(),
      data: OperationData(
        name: 'AoT',
        action: Attack(strength: 42),
        type: ActionType.attack,
      ),
    ),
  );

  final Operation o2 = await dorm.operations.repository.peek('543f2f8da023');

  final int value;
  switch (operation.type) {
    case ActionType.attack:
      final Attack attack = operation.action as Attack;
      value = attack.strength;
      break;
    case ActionType.defense:
      final Defense defense = operation.action as Defense;
      value = defense.resistence;
      break;
    case ActionType.heal:
      final Healing healing = operation.action as Healing;
      value = healing.health;
      break;
  }
}

Unique identification #

The default identifier type is String. Declare $dorm$generateId directly on the annotated class to derive an identifier from the generated model. The method receives the model and the initially generated identity. The examples below show the supported method shape.

Simple

If

@Model(name: 'country', as: #countries)
abstract class _Country {
  @Field(name: 'name')
  String get name;
}

then

void main() async {
  final Country country = await dorm.countries.repository.put(
    Creation.auto(
      dependency: CountryDependency(),
      data: CountryData(name: 'Brazil'),
    ),
  );
  // uuid
  assert(country.id == '27f04af67a1f');
}

Composite

If

@Model(
  name: 'state',
  as: #states,
  primaryKey: [
    ExistingIdSpec(referTo: #countryId),
    ExistingIdSpec(referTo: #stateCode),
  ],
)
abstract class _State {
  @Field(name: 'country-id')
  String get countryId;

  @Field(name: 'state-code')
  String get stateCode;

  @Field(name: 'name')
  String get name;
}

then

void main() async {
  final State state = await dorm.states.repository.push(
    State(
      id: CompositeKey(['27f04af67a1f', 'RJ']),
      name: 'Rio de Janeiro',
      countryId: '27f04af67a1f',
      stateCode: 'RJ',
    ),
  );
  assert(state.id == CompositeKey(['27f04af67a1f', 'RJ']));
}

Same-as

If

@Model(name: 'capital', as: #capitals)
abstract class _Capital {
  static String $dorm$generateId(_Capital model, String id) => model.countryId;

  @Field(name: 'name')
  String get name;

  @ForeignField(name: 'country-id', referTo: _Country)
  String get countryId;
}

then

void main() async {
  final Capital capital = await dorm.capitals.repository.put(
    Creation.auto(
      dependency: CapitalDependency(countryId: '27f04af67a1f'),
      data: CapitalData(name: 'Brasilia'),
    ),
  );
  // countryId
  assert(capital.id == '27f04af67a1f');
}

Custom

If

@Model(name: 'citizen', as: #citizens)
abstract class _Citizen {
  static String $dorm$generateId(_Citizen data, String id) =>
      data.visaCode ?? data.socialSecurity ?? id;
  @Field(name: 'name')
  String get name;

  @Field(name: 'is-foreigner', defaultValue: false)
  bool get isForeigner;

  @Field(name: 'visa-code')
  String? get visaCode;

  @Field(name: 'ssn')
  String? get socialSecurity;

  @ForeignField(name: 'country-id', referTo: _Country)
  String get countryId;
}

then

void main() async {
  final Citizen c1 = await dorm.citizens.repository.put(
    Creation.auto(
      dependency: CitizenDependency(countryId: '27f04af67a1f'),
      data: CitizenData(
        name: 'Rodrigo Maia',
        isForeigner: true,
        visaCode: '4bb6',
        socialSecurity: '11111111111',
      ),
    ),
  );
  // visaCode
  assert(c1.id == '4bb6');

  final Citizen c2 = await dorm.citizens.repository.put(
    Creation.auto(
      dependency: CitizenDependency(countryId: '27f04af67a1f'),
      data: CitizenData(
        name: 'Arthur Lira',
        isForeigner: false,
        visaCode: null,
        socialSecurity: '22222222222',
      ),
    ),
  );
  // socialSecurity
  assert(c2.id == '22222222222');

  final Citizen c3 = await dorm.citizens.repository.put(
    Creation.auto(
      dependency: CitizenDependency(countryId: '27f04af67a1f'),
      data: CitizenData(
        name: 'Capivara Filó',
        isForeigner: false,
        visaCode: null,
        socialSecurity: null,
      ),
    ),
  );
  // uuid
  assert(c3.id == 'b2a6304807a0');
}