Skip to content

Commit f60b310

Browse files
committed
feat(cloud_firestore_odm): Support injecting snapshot values
1 parent ebde7d2 commit f60b310

File tree

3 files changed

+145
-2
lines changed

3 files changed

+145
-2
lines changed

packages/cloud_firestore_odm/cloud_firestore_odm/lib/annotation.dart

+39
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,42 @@ class Collection<T> {
112112

113113
final String? name;
114114
}
115+
116+
/// Inject a value from Firestore in the annotated field or property.
117+
///
118+
/// Add this annotation to mutable fields or setters of types that
119+
/// are part of a [Collection].
120+
///
121+
/// See [FirestoreValue] for the types of values that can be injected.
122+
///
123+
/// ```dart
124+
/// @Inject(FirestoreField.id)
125+
/// String? _id;
126+
/// String? get id => _id;
127+
/// ```
128+
class Inject {
129+
const Inject(this.type);
130+
131+
final FirestoreValue type;
132+
}
133+
134+
/// Types of values that can be injected using [Inject].
135+
enum FirestoreValue {
136+
/// Injects the document's id.
137+
///
138+
/// Expects a field of type String or String?.
139+
id,
140+
141+
/// Injects the path to the document.
142+
///
143+
/// Expects a field of type String or String?.
144+
path,
145+
146+
/// Injects the id of the parent document if the document is in a
147+
/// subcollection.
148+
///
149+
/// Expects a field of type String or String?.
150+
/// Use the nullable version if you also use the containing type
151+
/// in root collections, otherwise the injection will throw.
152+
parentId,
153+
}

packages/cloud_firestore_odm/cloud_firestore_odm_generator/lib/src/collection_generator.dart

+77
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:analyzer/dart/constant/value.dart';
22
import 'package:analyzer/dart/element/element.dart';
3+
import 'package:analyzer/dart/element/nullability_suffix.dart';
34
import 'package:analyzer/dart/element/type.dart';
45
import 'package:build/build.dart';
56
import 'package:cloud_firestore_odm/annotation.dart';
@@ -26,6 +27,7 @@ class CollectionData {
2627
required this.queryableFields,
2728
required this.fromJson,
2829
required this.toJson,
30+
required this.injections,
2931
}) : collectionName =
3032
collectionName ?? ReCase(path.split('/').last).camelCase;
3133

@@ -62,12 +64,26 @@ class CollectionData {
6264
String Function(String json) fromJson;
6365
String Function(String value) toJson;
6466

67+
final List<FieldInjection> injections;
68+
6569
@override
6670
String toString() {
6771
return 'CollectionData(type: $type, collectionName: $collectionName, path: $path)';
6872
}
6973
}
7074

75+
class FieldInjection {
76+
const FieldInjection({
77+
required this.type,
78+
required this.name,
79+
required this.nullable,
80+
});
81+
82+
final FirestoreValue type;
83+
final String name;
84+
final bool nullable;
85+
}
86+
7187
class Data {
7288
Data(this.roots, this.subCollections);
7389

@@ -274,6 +290,60 @@ class CollectionGenerator extends ParserGenerator<void, Data, Collection> {
274290
}
275291
}
276292

293+
const injectTs = TypeChecker.fromRuntime(Inject);
294+
final injections = <FieldInjection>[];
295+
296+
final setters = collectionTargetElement.accessors;
297+
final fields = collectionTargetElement.fields;
298+
for (final f in [...setters, ...fields]) {
299+
final annotations = injectTs.annotationsOf(f);
300+
301+
if (annotations.isEmpty) continue;
302+
if (annotations.length > 1) {
303+
throw InvalidGenerationSourceError(
304+
'Only one @Inject annotation may be used for each field.',
305+
element: f,
306+
);
307+
}
308+
309+
if ((f is PropertyAccessorElement && !f.isSetter) ||
310+
(f is FieldElement && f.isFinal)) {
311+
throw InvalidGenerationSourceError(
312+
'@Inject fields or properties must be settable.',
313+
element: f,
314+
);
315+
}
316+
317+
final annotation = annotations.first;
318+
final annotationType = _parseInjectAnnotationType(annotation);
319+
320+
DartType? type;
321+
if (f is PropertyAccessorElement) {
322+
type = f.parameters[0].type;
323+
} else if (f is FieldElement) {
324+
type = f.type;
325+
}
326+
327+
if (type == null || !type.isDartCoreString) {
328+
throw InvalidGenerationSourceError(
329+
'Fields with @Inject must be of type String.',
330+
element: f,
331+
);
332+
}
333+
334+
// Names of setters have '=' appended.
335+
final name = f is PropertyAccessorElement
336+
? f.name.substring(0, f.name.length - 1)
337+
: f.name!;
338+
339+
final injection = FieldInjection(
340+
type: annotationType,
341+
name: name,
342+
nullable: type.nullabilitySuffix != NullabilitySuffix.none,
343+
);
344+
injections.add(injection);
345+
}
346+
277347
return CollectionData(
278348
type: type,
279349
path: path,
@@ -288,6 +358,7 @@ class CollectionGenerator extends ParserGenerator<void, Data, Collection> {
288358
},
289359
queryableFields: collectionTargetElement.fields
290360
.where((f) => f.isPublic)
361+
.whereNot((f) => injections.any((inj) => inj.name == f.name))
291362
.where(
292363
(f) =>
293364
f.type.isDartCoreString ||
@@ -299,9 +370,15 @@ class CollectionGenerator extends ParserGenerator<void, Data, Collection> {
299370
// TODO filter list other than LIst<string|bool|num>
300371
)
301372
.toList(),
373+
injections: injections,
302374
);
303375
}
304376

377+
FirestoreValue _parseInjectAnnotationType(DartObject annotation) {
378+
return FirestoreValue
379+
.values[annotation.getField('type')!.getField('index')!.toIntValue()!];
380+
}
381+
305382
@override
306383
Iterable<Object> generateForAll(void globalData) sync* {
307384
yield '''

packages/cloud_firestore_odm/cloud_firestore_odm_generator/lib/src/templates/collection_reference.dart

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'package:cloud_firestore_odm/annotation.dart';
2+
13
import '../collection_generator.dart';
24
import 'template.dart';
35

@@ -18,9 +20,11 @@ abstract class ${data.collectionReferenceInterfaceName}
1820
DocumentSnapshot<Map<String, Object?>> snapshot,
1921
SnapshotOptions? options,
2022
) {
21-
return ${data.fromJson('snapshot.data()!')};
23+
final data = ${data.fromJson('snapshot.data()!')};
24+
${_inject(data.injections)}
25+
return data;
2226
}
23-
27+
2428
static Map<String, Object?> toFirestore(
2529
${data.type} value,
2630
SetOptions? options,
@@ -179,4 +183,27 @@ factory ${data.collectionReferenceInterfaceName}([
179183
) : super(reference, reference);
180184
''';
181185
}
186+
187+
String _inject(List<FieldInjection> injections) {
188+
final buffer = StringBuffer();
189+
for (final injection in injections) {
190+
switch (injection.type) {
191+
case FirestoreValue.id:
192+
buffer.writeln('data.${injection.name} = snapshot.id;');
193+
break;
194+
case FirestoreValue.path:
195+
buffer.writeln(
196+
'data.${injection.name} = snapshot.reference.path;',
197+
);
198+
break;
199+
case FirestoreValue.parentId:
200+
buffer.writeln(
201+
"data.${injection.name} = snapshot.reference.parent.parent${injection.nullable ? '?' : '!'}.id;",
202+
);
203+
break;
204+
}
205+
}
206+
207+
return buffer.toString();
208+
}
182209
}

0 commit comments

Comments
 (0)