firestore_odm_builder 5.0.0 copy "firestore_odm_builder: ^5.0.0" to clipboard
firestore_odm_builder: ^5.0.0 copied to clipboard

Code generator for Firestore ODM annotations (v5). Generates type-safe converters, selectors, patch builders and collection accessors with exact Firestore semantics.

Firestore ODM πŸ”₯ #

firestore_odm β€” Sylphx Mark banner

Type-safe Firestore ODM for Dart/Flutter - zero reflection, code generation

pub package license

Zero reflection β€’ Full type safety β€’ Native Timestamp β€’ Measured performance

Documentation β€’ Getting Started β€’ Examples


πŸš€ Overview #

Firestore ODM transforms your Firestore development experience with type-safe, intuitive database operations that feel natural and productive.

The Problem:

// Standard cloud_firestore - Runtime errors waiting to happen
DocumentSnapshot doc = await FirebaseFirestore.instance
  .collection('users').doc('user123').get();
Map<String, dynamic>? data = doc.data();
String name = data?['name'];  // ❌ Runtime error if field doesn't exist
int age = data?['profile']['age'];  // ❌ Nested access is fragile

The Solution:

// Firestore ODM - Compile-time safety
User? user = await db.users('user123').get();
String name = user.name;  // βœ… IDE autocomplete, compile-time checking
int age = user.profile.age;  // βœ… Type-safe nested access

Result: Zero reflection, exact Firestore semantics, eliminate runtime errors.


πŸŽ‰ New in Version 5.0 (clean break, ADR-0002) #

  • Exact Firestore semantics - DateTime ↔ native Timestamp both directions; the ODM owns storage serialization.
  • Honest write verbs - create (returns the generated ID), set, patch (six FieldValue-shaped ops), delete. No sentinels, no modify() magic.
  • One-shot server-side aggregates - no fake client-side streaming.
  • Typed bulk writes - patchAll / deleteAll chunked to Firestore's 500-write batch limit.
  • Stable pagination - orderBy with the $.documentId tie-breaker selector.
  • Recorded benchmarks + emulator e2e lane - measured, not asserted.

πŸŽ‰ New in Version 4.0 #

Building on 3.0's performance foundation, 4.0 expands type coverage and ergonomics on top of a fully reworked code generator.

New in 4.0 #

  • βœ… Enum support - @JsonValue with both string and numeric values, enums in orderBy(), and default-value generation
  • πŸ§ͺ Firestore Pipelines (experimental) - collection.pipeline() for Enterprise-edition pipeline queries (see the release notes)
  • βœ… Automatic nested-class imports - filter, patch, aggregate, and orderBy selectors for nested types need no manual imports
  • βœ… Stronger nullable handling - nullable Map fields, and nested fromJson factories that accept nullable input no longer crash when a field is missing
  • βœ… Server timestamps on insert - not only on updates, and honored inside batches
  • βœ… Batch & transaction patch builders - atomic patch operations in runBatch / runTransaction
  • βœ… Reworked code generator - cleaner filter/patch/aggregate/orderBy builders and converters on a unified FieldPath model

Performance (measured, not asserted) #

The repo ships a recorded benchmark harness (apps/flutter_example/test/benchmarks_test.dart, run by the performance CI lane). Numbers are recorded per run; claims without measurements are not made.

Metric How it is measured
Serialization round-trip BENCH serialization_roundtrip_us_per_op (Stopwatch, 10k iterations)
Patch operation latency BENCH patch_op_ms_per_op (1000 ops against the test double)
Runtime overhead Zero reflection β€” all magic happens at compile time

Carried over from 3.0 #

  • βœ… Full generic model support - generic classes with type-safe patch operations and converter-argument threading (freezed-style toT/fromT)
  • βœ… JsonKey subset (name, ignore, includeFromJson/includeToJson) and @JsonConverter β€” documented honestly, no silent partial support
  • βœ… ODM-owned storage serialization - native Timestamp both directions; your model's toJson/fromJson remain for JSON interchange only

⚑ Key Features #

Type Safety Revolution #

Feature Standard Firestore Firestore ODM
Type Safety ❌ Map<String, dynamic> βœ… Strong types throughout
Query Building ❌ String-based, error-prone βœ… Type-safe with IDE support
Data Updates ❌ Manual map construction βœ… Two powerful strategies
Generic Support ❌ No generic handling βœ… Full generic models
Aggregations ❌ Basic count only βœ… One-shot count/sum/average (server-side)
Pagination ❌ Manual, risky βœ… Smart Builder, zero risk
Transactions ❌ Manual read-before-write βœ… Automatic deferred writes
Runtime Errors ❌ Common βœ… Eliminated at compile-time

Lightning Fast Code Generation #

  • πŸš€ Inline-first optimized - Callables and Dart extensions for maximum performance
  • πŸ“¦ Small generated surface - one unified codegen pipeline per model
  • ⚑ Measured performance - recorded benchmark harness in CI
  • πŸ”„ Model reusability - Same model works in collections and subcollections
  • ⏱️ Sub-second generation - Complex schemas compile in under 1 second
  • 🎯 Zero runtime overhead - All magic happens at compile time (no reflection)

Revolutionary Features #

Smart Builder Pagination - Eliminates common Firestore pagination bugs:

// Get first page with ordering
final page1 = await db.users
  .orderBy(($) => ($.followers(descending: true), $.name()))
  .limit(10)
  .get();

// Get next page with perfect type-safety - zero inconsistency risk
final page2 = await db.users
  .orderBy(($) => ($.followers(descending: true), $.name()))
  .startAfterObject(page1.last) // Auto-extracts cursor values
  .limit(10)
  .get();

One-shot Server-Side Aggregations (ADR-0002 β€” no fake client-side streaming; server-side aggregate streams are added only when cloud_firestore exposes them):

final stats = await db.users
  .where(($) => $.isActive(isEqualTo: true))
  .aggregate(($) => (
    count: $.count(),
    averageAge: $.age.average(),
    totalFollowers: $.profile.followers.sum(),
  ))
  .get();
print('${stats.count} users, avg ${stats.averageAge}');


---

## πŸ”₯ Before vs After

### Smart Query Building
```dart
// ❌ Standard - String-based field paths, typos cause runtime errors
final result = await FirebaseFirestore.instance
  .collection('users')
  .where('isActive', isEqualTo: true)
  .where('profile.followers', isGreaterThan: 100)
  .where('age', isLessThan: 30)
  .get();
// βœ… ODM - Type-safe query builder with IDE support
final result = await db.users
  .where(($) =>
    $.isActive(isEqualTo: true) &
    $.profile.followers(isGreaterThan: 100) &
    $.age(isLessThan: 30),
  )
  .get();

Intelligent Updates #

// ❌ Standard - Manual map construction, error-prone
await userDoc.update({
  'profile.followers': FieldValue.increment(1),
  'tags': FieldValue.arrayUnion(['verified']),
  'lastLogin': FieldValue.serverTimestamp(),
});
// βœ… ODM - Explicit typed patch operations (ADR-0002)
await userDoc.patch((p) => [
  p.profile.followers.increment(1),
  p.age.increment(1),
  p.tags.arrayUnion(['premium', 'active']), // atomic array union
  p.scores.arrayRemove([0, -1]),            // atomic array remove
  p.lastLogin.serverTimestamp(),            // server-set time
  p.name.set('Renamed'),                    // plain set
  p.oldField.delete(),                      // field delete
]);

Read-modify-write belongs in transactions, where it is safe:

await db.runTransaction((tx) async {
  final txUsers = db.users.inTransaction(tx);
  final user = await txUsers('jane').get();
  txUsers('jane').patch((p) => [p.age.increment(1)]);
});

πŸ“¦ Installation #

1. Add Dependencies #

dart pub add firestore_odm
dart pub add dev:firestore_odm_builder
dart pub add dev:build_runner

You'll also need a JSON serialization solution:

# If using Freezed (recommended)
dart pub add freezed_annotation
dart pub add dev:freezed
dart pub add dev:json_serializable

# If using plain classes
dart pub add json_annotation
dart pub add dev:json_serializable

2. Configure json_serializable (Critical for Nested Models) #

⚠️ Important: If you're using models with nested objects (especially with Freezed), you must create a build.yaml file next to your pubspec.yaml:

# build.yaml
targets:
  $default:
    builders:
      json_serializable:
        options:
          explicit_to_json: true

Why is this required? Without this configuration, json_serializable generates broken toJson() methods for nested objects. Instead of proper JSON, you'll get Instance of 'NestedClass' stored in Firestore, causing data corruption and deserialization failures.

When you need this:

  • βœ… Using nested Freezed classes
  • βœ… Using nested objects with json_serializable
  • βœ… Working with complex object structures
  • βœ… Encountering "Instance of..." in Firestore console

Alternative: Add @JsonSerializable(explicitToJson: true) to individual classes if you can't use global configuration.


πŸš€ Quick Start #

1. Define Your Model #

// lib/models/user.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    @DocumentIdField() required String id,
    required String name,
    required String email,
    required int age,
    DateTime? lastLogin,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

2. Define Your Schema #

// lib/schema.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'models/user.dart';

part 'schema.odm.dart';

@Schema()
@Collection<User>("users")
final appSchema = _$AppSchema;

3. Generate Code #

dart run build_runner build --delete-conflicting-outputs

4. Start Using #

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_odm/firestore_odm.dart';
import 'schema.dart';

final firestore = FirebaseFirestore.instance;
final db = FirestoreODM(appSchema, firestore: firestore);

// Create a user with an explicit ID (full replace)
await db.users.set(User(
  id: 'jane',
  name: 'Jane Smith',
  email: 'jane@example.com',
  age: 28,
));

// Create a user with a server-generated ID (the ID is returned)
final id = await db.users.create(User(
  id: '',
  name: 'John Doe',
  email: 'john@example.com',
  age: 30,
));

// Get a user
final user = await db.users('jane').get();
print(user?.name); // "Jane Smith"

// Type-safe queries
final youngUsers = await db.users
  .where(($) => $.age(isLessThan: 30))
  .orderBy(($) => $.name())
  .get();

🌟 Advanced Features #

Subcollections with Model Reusability #

@Schema()
@Collection<User>("users")
@Collection<Post>("posts")
@Collection<Post>("users/*/posts") // Same Post model, different location
final appSchema = AppSchema();

// Access a user's posts (path-derived accessor: users/*/posts -> usersPosts)
final userPosts = db.usersPosts('jane');
await userPosts.set(Post(id: 'post1', title: 'Hello World!'));

Bulk Operations (chunked to Firestore's 500-write limit) #

// Patch every match with the same typed operations
await db.users
  .where(($) => $.isPremium(isEqualTo: true))
  .patchAll([IncrementOperation(const FieldNode(components: ['points']), 100)]);

// Delete every match
await db.users
  .where(($) => $.isActive(isEqualTo: false))
  .deleteAll();

Smart Transactions #

await db.runTransaction((tx) async {
  // All reads happen first automatically
  final sender = await tx.users('user1').get();
  final receiver = await tx.users('user2').get();

  // Writes are automatically deferred until the end
  tx.users('user1').patch(($) => [$.balance.increment(-100)]);
  tx.users('user2').patch(($) => [$.balance.increment(100)]);
});

Atomic Batch Operations (typed create/set/patch/delete) #

// Automatic management - simple and clean
await db.runBatch((batch) {
  final users = db.users.inBatch(batch);
  users.set(newUser);
  db.posts.inBatch(batch).set(existingPost);
  db.usersPosts('user_id').inBatch(batch).set(userPost);
  users.delete('old_user');
});

// Manual management - fine-grained control
final batch = db.batch();
db.users.inBatch(batch).set(user1);
db.users.inBatch(batch).set(user2);
db.posts.inBatch(batch).patch('p1', (p) => [p.likes.increment(1)]);
await batch.commit();

Server Timestamps & Generated IDs (no sentinels, ADR-0002) #

// Server timestamps are explicit patch operations
await userDoc.patch((p) => [p.updatedAt.serverTimestamp()]);

// Server-generated document IDs come from create()
final id = await db.users.create(User(
  id: '',
  name: 'John Doe',
  email: 'john@example.com',
));

Server-set times use the explicit patch op (ADR-0002 β€” no sentinels): patch((p) => [p.updatedAt.serverTimestamp()]).


πŸ“Š Performance & Technical Excellence #

Optimized Code Generation #

Metric How it is measured
Serialization round-trip recorded BENCH serialization_roundtrip_us_per_op
Patch operation latency recorded BENCH patch_op_ms_per_op
Runtime overhead zero reflection β€” all magic at compile time

Claims without measurements are not made; the harness runs in the performance CI lane (apps/flutter_example/test/benchmarks_test.dart).

Advanced Capabilities #

  • βœ… Complex logical operations - and() and or()
  • βœ… Array operations - arrayContains, arrayContainsAny, whereIn
  • βœ… Range queries - Proper ordering constraints
  • βœ… Nested field access - Full type safety
  • βœ… Transaction support - Automatic deferred writes
  • βœ… Query/document streams - Real-time updates
  • βœ… Error handling - Meaningful compile-time messages
  • βœ… Testing support - fake_cloud_firestore integration

Flexible Data Modeling #

  • freezed (recommended) - Robust immutable classes
  • json_serializable - Plain Dart classes with full control
  • fast_immutable_collections - High-performance IList, IMap, ISet

πŸ§ͺ Testing #

Perfect integration with fake_cloud_firestore:

import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('user operations work correctly', () async {
    final firestore = FakeFirebaseFirestore();
    final db = FirestoreODM(appSchema, firestore: firestore);

    await db.users.set(User(
      id: 'test',
      name: 'Test User',
      email: 'test@example.com',
      age: 25,
    ));

    final user = await db.users('test').get();
    expect(user?.name, 'Test User');
  });
}

πŸ—ΊοΈ Roadmap #

βœ… Completed (3.0 β†’ 4.0)

  • βœ… Full generic model support
  • βœ… Honest JsonKey subset + @JsonConverter
  • βœ… Recorded benchmark harness (no unmeasured perf claims)
  • βœ… Enum support β€” string & numeric @JsonValue, orderBy, defaults (4.0)
  • βœ… Automatic nested-class imports (4.0)
  • βœ… Batch & transaction patch builders (4.0)
  • βœ… Server timestamps on insert (4.0)
  • βœ… Production-ready stability

πŸš€ Next

  • ❌ Firestore Pipelines support
  • ❌ Full map field filtering, ordering, and aggregation
  • ❌ Nested map support
  • ❌ Enhanced documentation

🀝 Support #

GitHub Issues pub.dev

Show Your Support: ⭐ Star β€’ πŸ‘€ Watch β€’ πŸ› Report bugs β€’ πŸ’‘ Suggest features β€’ πŸ”€ Contribute


πŸ“„ License #

MIT Β© Sylphx


πŸ™ Credits #

Built with:

Special thanks to the Flutter and Dart communities ❀️


Zero reflection. Type-safe. Production-ready.
The Firestore ODM that actually scales

sylphx.com β€’ @SylphxAI β€’ hi@sylphx.com

0
likes
140
points
254
downloads

Documentation

Documentation
API reference

Publisher

verified publishersylphx.com

Weekly Downloads

Code generator for Firestore ODM annotations (v5). Generates type-safe converters, selectors, patch builders and collection accessors with exact Firestore semantics.

Repository (GitHub)
View/report issues

Topics

#firestore #odm #code-generation #build-runner #flutter

License

MIT (license)

Dependencies

analyzer, build, code_builder, firestore_odm_annotation, json_annotation, source_gen

More

Packages that depend on firestore_odm_builder