Skip to content

Commit b037a59

Browse files
committed
feat: add support for Firestore data bundles
This commit introduces the `BundleBuilder` class, which allows for the creation of Firestore data bundles. Bundles can include document snapshots and named query snapshots. The `BundleBuilder` serializes these elements into a length-prefixed JSON format that can be served to clients for pre-loading data, enabling faster initial load times and offline access. The implementation includes: - `BundleBuilder` to add documents and queries. - `build()` method to generate the final `Uint8List` bundle. - Internal models for bundle elements (`BundleMetadata`, `BundledDocumentMetadata`, `NamedQuery`, etc.). - Helper methods for JSON serialization of Firestore types. - New unit and integration tests for the bundling functionality.
1 parent b26eb76 commit b037a59

File tree

7 files changed

+1719
-324
lines changed

7 files changed

+1719
-324
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import 'dart:async';
2+
3+
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
4+
import 'package:googleapis_firestore/googleapis_firestore.dart';
5+
6+
/// Main entry point for all Firestore examples
7+
Future<void> firestoreExample(FirebaseApp admin) async {
8+
print('\n### Firestore Examples ###\n');
9+
10+
await basicFirestoreExample(admin);
11+
await multiDatabaseExample(admin);
12+
await bulkWriterExamples(admin);
13+
await bundleBuilderExample(admin);
14+
}
15+
16+
/// Example 1: Basic Firestore operations with default database
17+
Future<void> basicFirestoreExample(FirebaseApp admin) async {
18+
print('> Basic Firestore operations (default database)...\n');
19+
20+
final firestore = admin.firestore();
21+
22+
try {
23+
final collection = firestore.collection('users');
24+
await collection.doc('123').set({'name': 'John Doe', 'age': 27});
25+
final snapshot = await collection.get();
26+
for (final doc in snapshot.docs) {
27+
print('> Document data: ${doc.data()}');
28+
}
29+
} catch (e) {
30+
print('> Error: $e');
31+
}
32+
print('');
33+
}
34+
35+
/// Example 2: Multi-database support
36+
Future<void> multiDatabaseExample(FirebaseApp admin) async {
37+
print('### Multi-Database Examples ###\n');
38+
39+
// Named database
40+
print('> Using named database "my-database"...\n');
41+
final namedFirestore = admin.firestore(databaseId: 'my-database');
42+
43+
try {
44+
final collection = namedFirestore.collection('products');
45+
await collection.doc('product-1').set({
46+
'name': 'Widget',
47+
'price': 19.99,
48+
'inStock': true,
49+
});
50+
print('> Document written to named database\n');
51+
52+
final doc = await collection.doc('product-1').get();
53+
if (doc.exists) {
54+
print('> Retrieved from named database: ${doc.data()}');
55+
}
56+
} catch (e) {
57+
print('> Error with named database: $e');
58+
}
59+
60+
// Multiple databases simultaneously
61+
print('\n> Demonstrating multiple database access...\n');
62+
try {
63+
final defaultDb = admin.firestore();
64+
final analyticsDb = admin.firestore(databaseId: 'analytics-db');
65+
66+
await defaultDb.collection('users').doc('user-1').set({
67+
'name': 'Alice',
68+
'email': 'alice@example.com',
69+
});
70+
71+
await analyticsDb.collection('events').doc('event-1').set({
72+
'type': 'page_view',
73+
'timestamp': DateTime.now().toIso8601String(),
74+
'userId': 'user-1',
75+
});
76+
77+
print('> Successfully wrote to multiple databases');
78+
} catch (e) {
79+
print('> Error with multiple databases: $e');
80+
}
81+
print('');
82+
}
83+
84+
/// BulkWriter examples demonstrating common patterns
85+
Future<void> bulkWriterExamples(FirebaseApp admin) async {
86+
print('### BulkWriter Examples ###\n');
87+
88+
final firestore = admin.firestore();
89+
90+
await bulkWriterBasicExample(firestore);
91+
await bulkWriterErrorHandlingExample(firestore);
92+
}
93+
94+
/// Basic BulkWriter usage
95+
Future<void> bulkWriterBasicExample(Firestore firestore) async {
96+
print('> Basic BulkWriter usage...\n');
97+
98+
try {
99+
final bulkWriter = firestore.bulkWriter();
100+
101+
// Queue multiple write operations (don't await individual operations)
102+
for (var i = 0; i < 10; i++) {
103+
unawaited(
104+
bulkWriter.set(firestore.collection('bulk-demo').doc('item-$i'), {
105+
'name': 'Item $i',
106+
'index': i,
107+
'createdAt': DateTime.now().toIso8601String(),
108+
}),
109+
);
110+
}
111+
112+
await bulkWriter.close();
113+
print('> Successfully wrote 10 documents in bulk\n');
114+
} catch (e) {
115+
print('> Error: $e');
116+
}
117+
}
118+
119+
/// BulkWriter with error handling and retry logic
120+
Future<void> bulkWriterErrorHandlingExample(Firestore firestore) async {
121+
print('> BulkWriter with error handling and retry logic...\n');
122+
123+
try {
124+
final bulkWriter = firestore.bulkWriter();
125+
126+
var successCount = 0;
127+
var errorCount = 0;
128+
129+
bulkWriter.onWriteResult((ref, result) {
130+
successCount++;
131+
print(' ✓ Success: ${ref.path} at ${result.writeTime}');
132+
});
133+
134+
bulkWriter.onWriteError((error) {
135+
errorCount++;
136+
print(' ✗ Error: ${error.documentRef.path} - ${error.message}');
137+
138+
// Retry on transient errors, but not more than 3 times
139+
if (error.failedAttempts < 3 &&
140+
(error.code.name == 'unavailable' || error.code.name == 'aborted')) {
141+
print(' → Retrying (attempt ${error.failedAttempts + 1})...');
142+
return true;
143+
}
144+
return false;
145+
});
146+
147+
// Mix of operations (queue them, don't await)
148+
// Use set() instead of create() to make example idempotent
149+
unawaited(
150+
bulkWriter.set(firestore.collection('orders').doc('order-1'), {
151+
'status': 'pending',
152+
'total': 99.99,
153+
}),
154+
);
155+
156+
unawaited(
157+
bulkWriter.set(firestore.collection('orders').doc('order-2'), {
158+
'status': 'completed',
159+
'total': 149.99,
160+
}),
161+
);
162+
163+
final orderRef = firestore.collection('orders').doc('order-3');
164+
await orderRef.set({'status': 'processing'});
165+
166+
unawaited(
167+
bulkWriter.update(orderRef, {
168+
FieldPath(const ['status']): 'shipped',
169+
FieldPath(const ['shippedAt']): DateTime.now().toIso8601String(),
170+
}),
171+
);
172+
173+
unawaited(
174+
bulkWriter.delete(firestore.collection('orders').doc('order-to-delete')),
175+
);
176+
177+
await bulkWriter.close();
178+
179+
print('\n> BulkWriter completed:');
180+
print(' - Successful writes: $successCount');
181+
print(' - Failed writes: $errorCount\n');
182+
} catch (e) {
183+
print('> Error: $e');
184+
}
185+
}
186+
187+
/// BundleBuilder example demonstrating data bundle creation
188+
Future<void> bundleBuilderExample(FirebaseApp admin) async {
189+
print('### BundleBuilder Example ###\n');
190+
191+
final firestore = admin.firestore();
192+
193+
try {
194+
print('> Creating a data bundle...\n');
195+
196+
// Create a bundle
197+
final bundle = firestore.bundle('example-bundle');
198+
199+
// Create and add some sample documents
200+
final collection = firestore.collection('bundle-demo');
201+
202+
// Add individual documents
203+
await collection.doc('user-1').set({
204+
'name': 'Alice Smith',
205+
'role': 'admin',
206+
'lastLogin': DateTime.now().toIso8601String(),
207+
});
208+
209+
await collection.doc('user-2').set({
210+
'name': 'Bob Johnson',
211+
'role': 'user',
212+
'lastLogin': DateTime.now().toIso8601String(),
213+
});
214+
215+
await collection.doc('user-3').set({
216+
'name': 'Charlie Brown',
217+
'role': 'user',
218+
'lastLogin': DateTime.now().toIso8601String(),
219+
});
220+
221+
// Get snapshots and add to bundle
222+
final doc1 = await collection.doc('user-1').get();
223+
final doc2 = await collection.doc('user-2').get();
224+
final doc3 = await collection.doc('user-3').get();
225+
226+
bundle.addDocument(doc1);
227+
bundle.addDocument(doc2);
228+
bundle.addDocument(doc3);
229+
230+
print(' ✓ Added 3 documents to bundle');
231+
232+
// Add a query to the bundle
233+
final query = collection.where('role', WhereFilter.equal, 'user');
234+
final querySnapshot = await query.get();
235+
236+
bundle.addQuery('regular-users', querySnapshot);
237+
238+
print(' ✓ Added query "regular-users" to bundle');
239+
240+
// Build the bundle
241+
final bundleData = bundle.build();
242+
243+
print('\n> Bundle created successfully!');
244+
print(' - Bundle size: ${bundleData.length} bytes');
245+
print(' - Contains: 3 documents + 1 named query');
246+
print('\n You can now:');
247+
print(' - Serve this bundle via CDN');
248+
print(' - Save to a file for static hosting');
249+
print(' - Send to clients for offline-first apps');
250+
print(' - Cache and reuse across multiple client sessions\n');
251+
252+
// Example: Save to file (commented out)
253+
// import 'dart:io';
254+
// await File('bundle.txt').writeAsBytes(bundleData);
255+
256+
// Clean up
257+
await collection.doc('user-1').delete();
258+
await collection.doc('user-2').delete();
259+
await collection.doc('user-3').delete();
260+
} catch (e) {
261+
print('> Error creating bundle: $e');
262+
}
263+
}

0 commit comments

Comments
 (0)