Cindel Generator
Source generator for Cindel schemas, serializers, typed collections, query builders, filters, projections, and native typed document hooks.
Overview | Setup | Model Shape | Freezed Models | Generated API | Indexes | Embedded Objects | Enums
Most applications use
cindel_generatoras adev_dependencytogether withbuild_runner. It reads annotations from your model classes and emits the*.g.dartfiles consumed by thecindelruntime.
Overview
cindel_generator turns annotated Dart classes into the code Cindel needs for
typed database access:
- Collection schema metadata.
- Generated typed document serializers and deserializers.
- Compact binary serializers and deserializers.
- Native typed document readers and writers when the field layout supports it.
- Typed collection accessors on
CindelDatabase. - Indexed
where()query helpers. filter()query helpers for persisted fields.- Sorting, distinct, property projection, and aggregate helpers.
- Composite index equality helpers.
- Embedded object conversion helpers.
- Embedded object and embedded object list native reader/writer hooks when the model layout supports native typed documents.
- Nested filter helpers for single embedded objects and embedded object list elements.
The package is a build-time tool. It does not open databases and it does not ship native binaries.
Setup
For Flutter apps, depend on Cindel and the native library package at runtime, then add the generator as a dev dependency:
dependencies:
cindel: ^0.9.1
cindel_flutter_libs: ^0.9.1
dev_dependencies:
build_runner: ^2.15.0
cindel_generator: ^0.9.1
Pure Dart packages can depend on cindel directly and provide a native library
path with CINDEL_NATIVE_LIBRARY when needed.
Basic Usage
Create a model file with a part directive and Cindel annotations:
import 'package:cindel/cindel.dart';
part 'user.g.dart';
@Collection(name: 'users')
class User {
Id dbId = autoIncrement;
@Index(unique: true)
late String email;
@index
late String name;
bool active = true;
}
Run the generator:
dart run build_runner build --delete-conflicting-outputs
Then use the generated schema and typed collection API:
final db = await Cindel.open(
directory: directory.path,
schemas: [UserSchema],
);
final user = User()
..name = 'Jhon Doe'
..email = 'jhon@example.com';
await db.users.put(user);
final saved = await db.users.where().emailEqualTo('jhon@example.com').findFirst();
Model Shape
Generated collections must follow the rules enforced by the generator:
@Collectioncan only be used on concrete classes, except supported Freezed primary-factory models.- A collection must declare at least one persisted field.
- A collection must declare exactly one persisted field named
dbId. - A collection needs either an unnamed constructor with no parameters or an unnamed constructor with parameters for every persisted field.
- Collections with final persisted fields need constructor parameters for every persisted field.
- Fields annotated with
@ignoreare excluded from persistence. @Namecan override the persisted collection or field name while generated Dart APIs continue to use the Dart identifier.
Supported persisted field shapes are:
bool,int,double, andString.DateTimeandDuration.- Enums.
- Embedded objects annotated with
@Embedded. - Nullable variants of supported shapes.
- Lists of supported non-list shapes, including embedded objects.
Nested lists are not supported.
Example persisted-name override:
@Name('accounts')
@collection
class Account {
Id dbId = autoIncrement;
@Name('user_name')
@Index(unique: true)
late String username;
}
Freezed Models
The generator supports Freezed classic classes when they expose concrete final fields:
import 'package:cindel/cindel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
@Collection(name: 'users')
class User with _$User {
const User({
required this.dbId,
required this.email,
required this.name,
});
@override
final Id dbId;
@override
@Index(unique: true)
final String email;
@override
final String name;
}
It also supports the common Freezed primary factory style by reading persisted properties from the unnamed factory constructor:
import 'package:cindel/cindel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
@Collection(name: 'users')
abstract class User with _$User {
const factory User({
required Id dbId,
required String email,
@Index(unique: true) required String username,
@Enumerated(CindelEnumType.ordinal) required UserStatus status,
@Default(true) bool active,
@ignore String? transientNote,
}) = _User;
}
For primary factory models, Cindel annotations such as @Index,
@Enumerated, and @ignore can be placed on factory parameters. Ignored
parameters must be optional so generated hydration can rebuild the object.
IMPORTANT: Freezed union/sealed multi-constructor models are not supported.
Generated API
For a User collection, the generator emits a schema named UserSchema and a
typed database accessor:
final users = db.users;
It also emits conversion functions used by the runtime:
- Dart object to generated Cindel document.
- Generated Cindel document to Dart object.
- Dart object to compact binary document.
- Compact binary document to Dart object.
- Native typed writer and reader hooks when supported by the field layout.
- Id getter, and an id setter when the model can assign generated ids.
putBy...andputAllBy...helpers for unique replace indexes.
Generated query access starts from where() for indexed fields and
collection-level composite indexes:
final user = await db.users.where().emailEqualTo('jhon@example.com').findFirst();
Generated filter() helpers are available for persisted fields:
final activeUsers = await db.users
.filter()
.activeEqualTo(true)
.sortByName()
.findAll();
Generated query modifiers include dynamic optional / anyOf / allOf
filter composition, field sorting, descending sorting, distinct helpers, and
property query accessors:
final names = await db.users
.filter()
.activeEqualTo(true)
.sortByName()
.nameProperty()
.findAll();
List fields generate element and length helpers:
final tagged = await db.users
.filter()
.tagsElementEqualTo('flutter')
.findAll();
final emptyTags = await db.users.filter().tagsIsEmpty().findAll();
final shortTagLists = await db.users
.filter()
.tagsLengthLessThan(3, include: true)
.findAll();
Indexes
The generator reads @index, @Index(...), and collection-level
CompositeIndex(...) annotations.
Value Indexes
@index
late String name;
Value indexes generate equality helpers and range-style helpers when the field type supports range queries.
Unique Indexes
@Index(unique: true)
late String email;
Unique indexes generate the same lookup helpers and tell the runtime to enforce unique values.
replace defaults to false. Use @Index(unique: true) for a normal unique
index. Add replace: true only when the unique index should generate
natural-key upsert helpers and replace conflicting documents during writes:
@Index(unique: true, replace: true)
late String email;
The generated typed collection exposes helpers such as:
await db.users.putByEmail(user);
await db.users.putAllByEmail(users);
Hash Indexes
@Index(type: CindelIndexType.hash)
late String externalId;
Hash indexes generate equality helpers only.
Word Indexes
@Index(type: CindelIndexType.words)
late String bio;
Word indexes are supported for string fields.
Multi-Entry Indexes
@Index(type: CindelIndexType.multiEntry)
late List<String> tags;
Multi-entry indexes are supported for lists of primitive values, DateTime,
Duration, or enums.
Composite Indexes
@Collection(
indexes: [
CompositeIndex(['teamId', 'email'], unique: true),
],
)
class TeamMember {
Id dbId = autoIncrement;
late int teamId;
late String email;
late String name;
}
Composite indexes generate equality helpers for the configured field set.
When a composite index is both unique and replace: true, the generator also
emits putBy... and putAllBy... helpers for the composite key.
Embedded Objects
Embedded classes are converted as part of their parent document. They are value
objects, not root collections, and can be declared with @Embedded() or the
lowercase @embedded constant.
@embedded
class Address {
late String city;
late String country;
}
@embedded
class Contact {
String? name;
String? email;
Address? address;
}
@collection
class User {
Id dbId = autoIncrement;
late String name;
Contact? primaryContact;
List<Contact>? contacts;
}
For single embedded object fields, generated filters include nested object helpers. Helpers can continue into nested embedded objects:
final users = await db.users
.filter()
.primaryContact((contact) {
return contact.address((address) {
return address.cityEqualTo('Santo Domingo');
});
})
.findAll();
final team = await db.users
.filter()
.contactsElement((contact) {
return contact.address((address) {
return address.countryEqualTo('DO');
});
})
.findAll();
The generator also emits:
- embedded conversion helpers used by generated document and binary serializers,
- whole-object equality filters such as
primaryContactEqualTo(value), - embedded-list equality filters such as
contactsEqualTo(values), - embedded-list element equality filters such as
contactsElementEqualTo(value), - embedded-list nested element filters such as
contactsElement((contact) => ...), - native writer calls for embedded objects and embedded object lists,
- native reader calls for embedded objects and embedded object lists.
Embedded indexes are not supported by the generator. @Index inside an
embedded class is rejected. Put indexes on root collection fields instead.
Enums
The generator supports enum fields and @Enumerated(...) strategies.
enum UserRole { admin, editor, viewer }
@collection
class User {
Id dbId = autoIncrement;
@Enumerated(CindelEnumType.name)
late UserRole role;
}
For value-based enum persistence:
enum AccountStatus {
active('A'),
suspended('S');
const AccountStatus(this.code);
final String code;
}
@collection
class Account {
Id dbId = autoIncrement;
@Enumerated(CindelEnumType.value, valueField: 'code')
late AccountStatus status;
}
Builder Details
The package registers a build_runner builder named cindel_generator.
It uses source_gen as a shared part builder:
- Input:
.dartfiles. - Intermediate output:
.cindel.g.part. - Final user-facing output: the combined
*.g.dartpart file.
In normal projects, adding the dependency and running build_runner is enough.
Status
Cindel is in active pre-1.0 development. This generator follows the same release line as the runtime package and emits the native typed readers, writers, query helpers, and hydration hooks used by the optimized Cindel runtime.
Libraries
- cindel_generator
- Source generator entrypoints for Cindel schema and typed API generation.