cindel_generator 0.9.1
cindel_generator: ^0.9.1 copied to clipboard
Source generator for Cindel schemas, serializers, typed collections, and query builders.
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.