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_generator as a dev_dependency together with build_runner. It reads annotations from your model classes and emits the *.g.dart files consumed by the cindel runtime.

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: ^x.y.z
  cindel_flutter_libs: ^x.y.z

dev_dependencies:
  build_runner: ^2.15.0
  cindel_generator: ^x.y.z

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:

  • @Collection can 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 @ignore are excluded from persistence.
  • @Name can 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, and String.
  • DateTime and Duration.
  • 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... and putAllBy... 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: .dart files.
  • Intermediate output: .cindel.g.part.
  • Final user-facing output: the combined *.g.dart part file.

In normal projects, adding the dependency and running build_runner is enough.

Status

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.