Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

SylphxAI/firestore_odm

Open more actions menu

Firestore ODM πŸ”₯

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

pub package license

20% faster runtime β€’ 15% less code β€’ Zero reflection β€’ Full type safety

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, 20% faster runtime, eliminate runtime errors.


πŸŽ‰ New in Version 3.0!

The most stable and feature-complete release yet - over 90% of planned features complete!

Performance Improvements

Metric Improvement Impact
Runtime Performance 20% faster Optimized code generation
Generated Code 15% smaller Extension-based architecture
Compilation Speed <1 second Complex schemas compile instantly
Runtime Overhead Zero All magic at compile time

New Features & Capabilities

  • βœ… Full generic model support - Generic classes with type-safe patch operations
  • βœ… Complete JsonKey & JsonConverter support - Full serialization control
  • βœ… Automatic conversion fallbacks - JsonConverter optional in most cases
  • βœ… Enhanced map operations - Comprehensive map field support with atomic ops
  • βœ… 100+ new test cases - Rigorous testing for production stability

⚑ 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 βœ… Comprehensive + streaming
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
  • πŸ“¦ 15% less generated code - Smart generation without bloating your project
  • ⚑ 20% performance improvement - Optimized runtime execution
  • πŸ”„ 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

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();

Streaming Aggregations - Real-time aggregation subscriptions:

// Live statistics that update in real-time
db.users
  .where(($) => $.isActive(isEqualTo: true))
  .aggregate(($) => (
    count: $.count(),
    averageAge: $.age.average(),
    totalFollowers: $.profile.followers.sum(),
  ))
  .stream
  .listen((stats) {
    print('Live: ${stats.count} users, avg age ${stats.averageAge}');
  });

πŸ”₯ Before vs After

Smart Query Building

// ❌ 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(($) => $.and(
    $.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 - Two powerful update strategies

// 1. Patch - Explicit atomic operations (Best Performance)
await userDoc.patch(($) => [
  $.profile.followers.increment(1),
  $.tags.add('verified'),              // Add single element
  $.tags.addAll(['premium', 'active']), // Add multiple elements
  $.scores.removeAll([0, -1]),         // Remove multiple elements
  $.lastLogin.serverTimestamp(),
]);

// 2. Modify - Smart atomic detection (Read + Auto-detect operations)
await userDoc.modify((user) => user.copyWith(
  age: user.age + 1,              // Auto-detects -> FieldValue.increment(1)
  tags: [...user.tags, 'expert'], // Auto-detects -> FieldValue.arrayUnion()
  lastLogin: FirestoreODM.serverTimestamp,
));

πŸ“¦ 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 custom ID
await db.users.insert(User(
  id: 'jane',
  name: 'Jane Smith',
  email: 'jane@example.com',
  age: 28,
));

// Create a user with auto-generated ID
await db.users.insert(User(
  id: FirestoreODM.autoGeneratedId,
  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 user's posts
final userPosts = db.users('jane').posts;
await userPosts.insert(Post(id: 'post1', title: 'Hello World!'));

Bulk Operations

// Update all premium users using patch (best performance)
await db.users
  .where(($) => $.isPremium(isEqualTo: true))
  .patch(($) => [$.points.increment(100)]);

// Update all premium users using modify (read + auto-detect atomic)
await db.users
  .where(($) => $.isPremium(isEqualTo: true))
  .modify((user) => user.copyWith(points: user.points + 100));

// Delete inactive users
await db.users
  .where(($) => $.status(isEqualTo: 'inactive'))
  .delete();

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

// Automatic management - simple and clean
await db.runBatch((batch) {
  batch.users.insert(newUser);
  batch.posts.update(existingPost);
  batch.users('user_id').posts.insert(userPost);
  batch.users('old_user').delete();
});

// Manual management - fine-grained control
final batch = db.batch();
batch.users.insert(user1);
batch.users.insert(user2);
batch.posts.update(post);
await batch.commit();

Server Timestamps & Auto-Generated IDs

// Server timestamps using patch (best performance)
await userDoc.patch(($) => [$.lastLogin.serverTimestamp()]);

// Server timestamps using modify (read + smart detection)
await userDoc.modify((user) => user.copyWith(
  loginCount: user.loginCount + 1,  // Uses current value + auto-detects increment
  lastLogin: FirestoreODM.serverTimestamp,
));

// Auto-generated document IDs
await db.users.insert(User(
  id: FirestoreODM.autoGeneratedId, // Server generates unique ID
  name: 'John Doe',
  email: 'john@example.com',
));

⚠️ Server Timestamp Warning: FirestoreODM.serverTimestamp must be used exactly as-is. Any arithmetic operations (+, .add(), etc.) will create a regular DateTime instead of a server timestamp. See the Server Timestamps Guide for alternatives.


πŸ“Š Performance & Technical Excellence

Optimized Code Generation (v3.0)

Metric Value Benefit
Runtime Performance +20% Optimized execution paths
Generated Code Size -15% Smart generation without bloat
Compilation Time <1 second Complex schemas compile instantly
Runtime Overhead Zero All magic at compile time

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
  • βœ… Streaming subscriptions - 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.insert(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 (v3.0)

  • Full generic model support
  • Complete JsonKey & JsonConverter support
  • 20% runtime performance improvement
  • 15% reduction in generated code
  • 100+ new test cases
  • Production-ready stability

πŸš€ Next

  • Batch collection operations
  • Map field filtering, ordering, and aggregation
  • Nested map support
  • Performance monitoring
  • 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

About

πŸ”₯ Type-safe Firestore ODM for Dart/Flutter - code-generation with zero reflection

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published
Morty Proxy This is a proxified and sanitized view of the page, visit original site.