Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mainser/cindel/llms.txt

Use this file to discover all available pages before exploring further.

Cindel’s code generator reads your model annotations and emits fully-typed query helpers for every indexed and persisted field in your collection. You never write raw SQL or manually encode field names — instead you chain type-safe method calls that the generator verifies against your actual schema. This page describes both the index-backed where() path and the general filter() path, along with sorting, pagination, projections, aggregates, and the composition helpers used for dynamic query construction. where() requires the target field to have an @Index annotation. filter() works on any persisted field — indexed or not. For best performance on large collections, reach for where() first and layer filter() predicates only when you need conditions that go beyond what an index can provide.
Both where() and filter() chains terminate with a result method: findFirst(), findAll(), count(), deleteAll(), or updateAll().

Index-Backed Lookups with where()

Generated where() helpers compile down to native index queries that bypass full-collection scans. The available methods depend on the field type and index configuration:
// Exact match on a unique string index
final ada = await db.users
    .where()
    .emailEqualTo('ada@example.com')
    .findFirst();

// Inclusive range on a DateTime index
final recentUsers = await db.users
    .where()
    .createdAtBetween(
      DateTime(2024, 1, 1).toUtc(),
      DateTime(2024, 12, 31).toUtc(),
    )
    .findAll();
Every where() chain terminates with a result method such as findFirst() or findAll(). The generated helpers are named after the field — emailEqualTo, createdAtBetween, nameStartsWith, and so on.

General Filtering with filter()

filter() applies predicates to every persisted field including non-indexed ones. It also supports sorting and pagination:
// Boolean field equality
final activeUsers = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .findAll();

// String contains
final matches = await db.users
    .filter()
    .nameContains('Ada')
    .findAll();

String Predicates

String fields support contains, prefix (StartsWith), and suffix (EndsWith) predicates:
// Contains a substring
final byFragment = await db.users
    .filter()
    .nameContains('love')
    .findAll();

// Starts with a prefix (uses index range when @Index is present)
final byPrefix = await db.users
    .filter()
    .nameStartsWith('Ada')
    .findAll();

// Ends with a suffix
final bySuffix = await db.users
    .filter()
    .nameEndsWith('Lovelace')
    .findAll();

List Field Helpers

For List<T> fields, Cindel generates element membership and length predicates:
// Element equality — matches documents where any list item equals 'flutter'
final flutterUsers = await db.users
    .filter()
    .tagsElementEqualTo('flutter')
    .findAll();

// Length comparison — matches documents where the list is non-empty
final taggedUsers = await db.users
    .filter()
    .tagsLengthGreaterThan(0)
    .findAll();

Embedded Object Filters

For fields typed as @embedded classes, the generator produces a nested callback builder that lets you filter on embedded fields at any depth:
// Filter on a direct embedded field
final messages = await db.emails
    .filter()
    .sender((recipient) => recipient.addressEqualTo('ada@example.com'))
    .findAll();

// Filter on a nested embedded field (list element → embedded object → field)
final secondary = await db.emails
    .filter()
    .recipientsElement((recipient) {
      return recipient.metadata((metadata) {
        return metadata.labelEqualTo('secondary');
      });
    })
    .findAll();

Composition Helpers

Three composition utilities let you build queries dynamically without if statements scattered across your code:

optional — conditional filter

Applies the builder only when condition is true, otherwise returns the query unchanged:
final results = await db.users
    .filter()
    .optional(search.isNotEmpty, (q) => q.nameContains(search))
    .findAll();

anyOf — OR across a list of values

Applies the builder once for each item and ORs the resulting filters together. An empty list matches nothing:
final byTags = await db.users
    .filter()
    .anyOf(selectedTags, (q, tag) => q.tagsElementEqualTo(tag))
    .findAll();

allOf — AND across a list of values

Applies the builder once for each item and ANDs the resulting filters together. An empty list returns the query unchanged:
final byRequiredTags = await db.users
    .filter()
    .allOf(requiredTags, (q, tag) => q.tagsElementEqualTo(tag))
    .findAll();

Terminal Operations

Every query chain ends with one of these:
MethodReturn typeDescription
findAll()Future<List<T>>Returns all matching objects.
findFirst()Future<T?>Returns the first matching object, or null.
count()Future<int>Returns the count of matching documents without hydrating objects.
deleteAll()Future<int>Deletes every matching document and returns the count.
updateAll(changes)Future<int>Applies field updates to every matching document and returns the count.

Querying Every Document — all()

all() starts a query over the entire collection with no index source. Combine it with filter(), sort, and projection:
final everyone = await db.users.all().findAll();

final maxId = await db.users.all().dbIdProperty().max();

Sorting

Generated sortBy* methods sort by a named field in ascending order; sortBy*Desc sorts descending. Chain multiple sort calls to apply secondary sort keys:
final sorted = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .findAll();

// Descending
final latestFirst = await db.users
    .all()
    .sortByCreatedAtDesc()
    .findAll();

Pagination

.offset(n) skips n results after all filters, sorting, and distinct are applied. .limit(n) caps the result count. Chain them together for page-based navigation:
// Page 3 of a 10-per-page list (zero-indexed: pages 0, 1, 2 ...)
final page3 = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .offset(20)
    .limit(10)
    .findAll();

Projections

Chain a generated *Property() method to return only a single field value instead of full objects. This avoids deserializing fields you don’t need:
// Returns List<String> — only the name field
final names = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .offset(20)
    .limit(10)
    .nameProperty()
    .findAll();

Aggregates

Property queries expose aggregate terminal operations:
// Maximum id in the collection
final maxId = await db.users
    .all()
    .dbIdProperty()
    .max();

// Count without hydrating objects (faster than findAll().length)
final count = await db.users
    .filter()
    .activeEqualTo(true)
    .count();

Mutation via Query

deleteAll

Delete every document matching the current filter, returning the number of deleted documents:
final deleted = await db.users
    .filter()
    .activeEqualTo(false)
    .deleteAll();

updateAll

Apply a map of field updates to every matching document atomically, returning the count of updated documents:
final updated = await db.users
    .filter()
    .activeEqualTo(false)
    .updateAll({'active': true});
The keys in the map must match the stored field names (as declared in the schema, respecting any @Name overrides). Values must be null, bool, int, double, String, List, or Map.

Build docs developers (and LLMs) love