dorm_framework
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.