dorm_framework

dorm_framework on pub.dev dorm_framework pub points dorm_framework popularity dorm_framework likes dorm_framework documentation dORM repository License Dart CI

dorm_annotations dorm_generator dorm_memory_database dorm_bloc_database dorm_firebase_database dorm_firestore_database dorm_http_database dorm_mongo_database dorm_mysql_database dorm_postgres_database dorm_sqlite_database dorm_example

dorm_framework defines the engine-independent types used by dORM applications. It describes how generated models become entities and repositories, how reads and writes are expressed, and how engines expose their backend-specific execution through one common contract.

Install

Add the framework to the application:

dart pub add dorm_framework

A working generated model also needs dorm_annotations, dorm_generator, build_runner, and one engine. The framework package is the center of those contracts; it does not open a database by itself.

The model and repository boundary

dORM separates three values that are easy to confuse:

  • Data is input used to create or update a model.
  • Model is an identified value that can be persisted or returned by a read.
  • Dependency carries identities needed to construct related data.
  • Entity connects generated data/model types to schema and repository behavior.
  • Repository is the application-facing object for CRUD and reads.

A typical generated flow looks like this:

final User user = await dorm.users.repository.put(
  Creation.auto(
    dependency: const UserDependency(),
    data: UserData(
      username: 'ada',
      email: 'ada@example.com',
      profile: Profile(
        name: 'Ada Lovelace',
        birthDate: DateTime(1815, 12, 10),
        bio: 'Mathematician',
      ),
    ),
  ),
);

final User? loaded = await dorm.users.repository.peek(user.id);

Creation resolves the identity and passes a ResolvedCreation to the generated Entity. The resulting Model is the identified form returned by the repository.

Creating the generated facade

The generated Dorm class is parameterized by the engine query and page types. Type inference normally handles both:

final Engine engine = Engine();
final dorm = Dorm(engine);

The facade exposes generated accessors such as dorm.users and dorm.products. Each accessor carries its Entity, Repository, schema fields, and relationship paths.

Creating and updating data

Use put for new Data values. Use push when a Model already has its final identity:

final User created = await dorm.users.repository.put(
  Creation.auto(
    dependency: const UserDependency(),
    data: UserData(
      username: 'ada',
      email: 'ada@example.com',
      profile: Profile(
        name: 'Ada Lovelace',
        birthDate: DateTime(1815, 12, 10),
        bio: 'Mathematician',
      ),
    ),
  ),
);

await dorm.users.repository.push(
  created.copyWith(email: 'ada@lovelace.org'),
);

Use Creation.explicit when the application already owns the final identity:

final User imported = await dorm.users.repository.put(
  Creation.explicit(
    dependency: const UserDependency(),
    data: UserData(
      username: 'grace',
      email: 'grace@example.com',
      profile: Profile(
        name: 'Grace Hopper',
        birthDate: DateTime(1906, 12, 9),
        bio: 'Computer scientist',
      ),
    ),
    identity: 'external-user-42',
  ),
);

For a model whose backend supplies its identity, Creation.auto uses the declared DatabaseGeneratedIdSpec and returns the model after the backend has returned the key.

Reading, filtering, sorting, and pages

Filters resolve application fields through generated FieldSchema values:

final List<User> users = await dorm.users.repository.peekAll(
  Filter.text('ada', field: UserEntity.fields.username),
  QueryOptions(
    orderBy: [
      OrderBy(UserEntity.fields.username),
    ],
  ),
);

The filter API describes the condition. The engine Query turns the resolved field name into SQL, a selector, a Firebase query, or an HTTP parameter. OffsetPageRequest is the page request accepted by current engines:

final Page<User> page = await dorm.users.repository.peekPage(
  const BaseFilter.empty(),
  const OffsetPageRequest(size: 20, offset: 0),
);

Relationships

Foreign fields become generated relationship paths. The application uses the same relationship vocabulary regardless of whether the selected engine joins SQL tables, reads documents, or performs several repository reads:

final List<Join<Cart, CartItem>> items = await dorm
    .relations
    .carts
    .items
    .peekAll();

Relationship cardinality is declared by the model metadata and the generated path. Backend query counts and relation optimizations can differ by engine.

Transactions

Engines that implement TransactionalEngine also generate TransactionalDorm:

final txDorm = TransactionalDorm(engine);

await txDorm.transaction((tx) async {
  final User? user = await tx.users.repository.peek(userId);
  if (user != null) {
    await tx.users.repository.push(user.copyWith(email: 'new@example.com'));
  }
});

The callback receives a temporary Dorm context. Streams are not available in that context. Engines without the capability keep the normal Dorm API.

Swapping engines

The generated model source is independent of the backend object. To change engines, construct a different Engine and regenerate only when the selected engine changes the generated type context:

final engine = Engine(databaseOrClient);
final dorm = Dorm(engine);

The repository calls stay the same. Backend-specific capabilities do not: identity types, live streams, transactions, query operators, schema setup, and atomicity depend on the selected engine.

Implementing an engine

A custom engine implements BaseEngine<Q, P>, creates references and relationships, and provides a concrete BaseQuery. Reference methods receive generated Entity metadata and perform the backend operation. The engine owns connection setup and backend-specific serialization details; the framework does not create or close external connections.

Use dorm_test from development code to exercise the portable contract. Keep engine-specific behavior in separate tests.

Important boundaries

dorm_framework does not execute schemas or migrations itself. It exposes optional migration contracts for dorm_migrations, while each adapter owns the backend-specific execution. It does not make every backend transactional or reactive. A stream may represent a live subscription or only the initial read, depending on the engine. Query features must be supported by the selected backend; dORM does not silently download and filter data on the client.

Learn more

Libraries

dorm_framework