@react-native-firebase/firestore 17.4.3 → 17.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [17.5.0](https://github.com/invertase/react-native-firebase/compare/v17.4.3...v17.5.0) (2023-05-11)
7
+
8
+ ### Features
9
+
10
+ - **firestore:** Firestore `Filter` instance. Use `Filter`, `Filter.or()` & `Filter.and()` in Firestore queries. ([#7045](https://github.com/invertase/react-native-firebase/issues/7045)) ([f7ec3d1](https://github.com/invertase/react-native-firebase/commit/f7ec3d1970f4fa8cc9752bb3cd8f1550b2a457c5))
11
+
6
12
  ### [17.4.3](https://github.com/invertase/react-native-firebase/compare/v17.4.2...v17.4.3) (2023-04-26)
7
13
 
8
14
  **Note:** Version bump only for package @react-native-firebase/firestore
@@ -26,6 +26,7 @@ import com.facebook.react.bridge.WritableMap;
26
26
  import com.google.android.gms.tasks.Task;
27
27
  import com.google.android.gms.tasks.Tasks;
28
28
  import com.google.firebase.firestore.FieldPath;
29
+ import com.google.firebase.firestore.Filter;
29
30
  import com.google.firebase.firestore.Query;
30
31
  import com.google.firebase.firestore.QuerySnapshot;
31
32
  import com.google.firebase.firestore.Source;
@@ -65,62 +66,139 @@ public class ReactNativeFirebaseFirestoreQuery {
65
66
  for (int i = 0; i < filters.size(); i++) {
66
67
  ReadableMap filter = filters.getMap(i);
67
68
 
68
- ArrayList fieldPathArray = Objects.requireNonNull(filter).getArray("fieldPath").toArrayList();
69
- String[] segmentArray = (String[]) fieldPathArray.toArray(new String[0]);
69
+ if (filter.hasKey("fieldPath")) {
70
+ ArrayList<Object> fieldPathArray =
71
+ Objects.requireNonNull(Objects.requireNonNull(filter).getArray("fieldPath"))
72
+ .toArrayList();
73
+ String[] segmentArray = (String[]) fieldPathArray.toArray(new String[0]);
74
+
75
+ FieldPath fieldPath = FieldPath.of(Objects.requireNonNull(segmentArray));
76
+ String operator = filter.getString("operator");
77
+ ReadableArray arrayValue = filter.getArray("value");
78
+ Object value = parseTypeMap(query.getFirestore(), Objects.requireNonNull(arrayValue));
79
+
80
+ switch (Objects.requireNonNull(operator)) {
81
+ case "EQUAL":
82
+ query = query.whereEqualTo(Objects.requireNonNull(fieldPath), value);
83
+ break;
84
+ case "NOT_EQUAL":
85
+ query = query.whereNotEqualTo(Objects.requireNonNull(fieldPath), value);
86
+ break;
87
+ case "GREATER_THAN":
88
+ query =
89
+ query.whereGreaterThan(
90
+ Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
91
+ break;
92
+ case "GREATER_THAN_OR_EQUAL":
93
+ query =
94
+ query.whereGreaterThanOrEqualTo(
95
+ Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
96
+ break;
97
+ case "LESS_THAN":
98
+ query =
99
+ query.whereLessThan(
100
+ Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
101
+ break;
102
+ case "LESS_THAN_OR_EQUAL":
103
+ query =
104
+ query.whereLessThanOrEqualTo(
105
+ Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
106
+ break;
107
+ case "ARRAY_CONTAINS":
108
+ query =
109
+ query.whereArrayContains(
110
+ Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
111
+ break;
112
+ case "ARRAY_CONTAINS_ANY":
113
+ query =
114
+ query.whereArrayContainsAny(
115
+ Objects.requireNonNull(fieldPath),
116
+ Objects.requireNonNull((List<Object>) value));
117
+ break;
118
+ case "IN":
119
+ query =
120
+ query.whereIn(
121
+ Objects.requireNonNull(fieldPath),
122
+ Objects.requireNonNull((List<Object>) value));
123
+ break;
124
+ case "NOT_IN":
125
+ query =
126
+ query.whereNotIn(
127
+ Objects.requireNonNull(fieldPath),
128
+ Objects.requireNonNull((List<Object>) value));
129
+ break;
130
+ }
131
+ } else if (filter.hasKey("operator") && filter.hasKey("queries")) {
132
+ query = query.where(applyFilterQueries(filter));
133
+ }
134
+ }
135
+ }
136
+
137
+ private Filter applyFilterQueries(ReadableMap filter) {
138
+ if (filter.hasKey("fieldPath")) {
139
+ String operator =
140
+ (String) Objects.requireNonNull(Objects.requireNonNull(filter).getString("operator"));
141
+ ReadableMap fieldPathMap = Objects.requireNonNull(filter.getMap("fieldPath"));
142
+ ReadableArray segments = Objects.requireNonNull(fieldPathMap.getArray("_segments"));
143
+ int arraySize = segments.size();
144
+ String[] segmentArray = new String[arraySize];
70
145
 
71
- FieldPath fieldPath = FieldPath.of(Objects.requireNonNull(segmentArray));
72
- String operator = filter.getString("operator");
146
+ for (int i = 0; i < arraySize; i++) {
147
+ segmentArray[i] = segments.getString(i);
148
+ }
149
+ FieldPath fieldPath = FieldPath.of(segmentArray);
73
150
  ReadableArray arrayValue = filter.getArray("value");
151
+
74
152
  Object value = parseTypeMap(query.getFirestore(), Objects.requireNonNull(arrayValue));
75
153
 
76
- switch (Objects.requireNonNull(operator)) {
154
+ switch (operator) {
77
155
  case "EQUAL":
78
- query = query.whereEqualTo(Objects.requireNonNull(fieldPath), value);
79
- break;
156
+ return Filter.equalTo(fieldPath, value);
80
157
  case "NOT_EQUAL":
81
- query = query.whereNotEqualTo(Objects.requireNonNull(fieldPath), value);
82
- break;
83
- case "GREATER_THAN":
84
- query =
85
- query.whereGreaterThan(
86
- Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
87
- break;
88
- case "GREATER_THAN_OR_EQUAL":
89
- query =
90
- query.whereGreaterThanOrEqualTo(
91
- Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
92
- break;
158
+ return Filter.notEqualTo(fieldPath, value);
93
159
  case "LESS_THAN":
94
- query =
95
- query.whereLessThan(Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
96
- break;
160
+ return Filter.lessThan(fieldPath, value);
97
161
  case "LESS_THAN_OR_EQUAL":
98
- query =
99
- query.whereLessThanOrEqualTo(
100
- Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
101
- break;
162
+ return Filter.lessThanOrEqualTo(fieldPath, value);
163
+ case "GREATER_THAN":
164
+ return Filter.greaterThan(fieldPath, value);
165
+ case "GREATER_THAN_OR_EQUAL":
166
+ return Filter.greaterThanOrEqualTo(fieldPath, value);
102
167
  case "ARRAY_CONTAINS":
103
- query =
104
- query.whereArrayContains(
105
- Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
106
- break;
168
+ return Filter.arrayContains(fieldPath, value);
107
169
  case "ARRAY_CONTAINS_ANY":
108
- query =
109
- query.whereArrayContainsAny(
110
- Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
111
- break;
170
+ assert value != null;
171
+ return Filter.arrayContainsAny(fieldPath, (List<?>) value);
112
172
  case "IN":
113
- query =
114
- query.whereIn(
115
- Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
116
- break;
173
+ assert value != null;
174
+ return Filter.inArray(fieldPath, (List<?>) value);
117
175
  case "NOT_IN":
118
- query =
119
- query.whereNotIn(
120
- Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
121
- break;
176
+ assert value != null;
177
+ return Filter.notInArray(fieldPath, (List<?>) value);
178
+ default:
179
+ throw new Error("Invalid operator");
122
180
  }
123
181
  }
182
+
183
+ String operator = Objects.requireNonNull(filter).getString("operator");
184
+ ReadableArray queries =
185
+ Objects.requireNonNull(Objects.requireNonNull(filter).getArray("queries"));
186
+ ArrayList<Filter> parsedFilters = new ArrayList<>();
187
+ int arraySize = queries.size();
188
+ for (int i = 0; i < arraySize; i++) {
189
+ ReadableMap map = queries.getMap(i);
190
+ parsedFilters.add(applyFilterQueries(map));
191
+ }
192
+
193
+ if (operator.equals("AND")) {
194
+ return Filter.and(parsedFilters.toArray(new Filter[0]));
195
+ }
196
+
197
+ if (operator.equals("OR")) {
198
+ return Filter.or(parsedFilters.toArray(new Filter[0]));
199
+ }
200
+
201
+ throw new Error("Missing 'Filter' instance return");
124
202
  }
125
203
 
126
204
  private void applyOrders(ReadableArray orders) {
@@ -51,33 +51,98 @@
51
51
 
52
52
  - (void)applyFilters {
53
53
  for (NSDictionary *filter in _filters) {
54
- NSArray *fieldPathArray = filter[@"fieldPath"];
54
+ if (filter[@"fieldPath"]) {
55
+ NSArray *fieldPathArray = filter[@"fieldPath"];
56
+
57
+ FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathArray];
58
+ NSString *operator= filter[@"operator"];
59
+ id value = [RNFBFirestoreSerialize parseTypeMap:_firestore typeMap:filter[@"value"]];
60
+ if ([operator isEqualToString:@"EQUAL"]) {
61
+ _query = [_query queryWhereFieldPath:fieldPath isEqualTo:value];
62
+ } else if ([operator isEqualToString:@"NOT_EQUAL"]) {
63
+ _query = [_query queryWhereFieldPath:fieldPath isNotEqualTo:value];
64
+ } else if ([operator isEqualToString:@"GREATER_THAN"]) {
65
+ _query = [_query queryWhereFieldPath:fieldPath isGreaterThan:value];
66
+ } else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
67
+ _query = [_query queryWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value];
68
+ } else if ([operator isEqualToString:@"LESS_THAN"]) {
69
+ _query = [_query queryWhereFieldPath:fieldPath isLessThan:value];
70
+ } else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
71
+ _query = [_query queryWhereFieldPath:fieldPath isLessThanOrEqualTo:value];
72
+ } else if ([operator isEqualToString:@"ARRAY_CONTAINS"]) {
73
+ _query = [_query queryWhereFieldPath:fieldPath arrayContains:value];
74
+ } else if ([operator isEqualToString:@"IN"]) {
75
+ _query = [_query queryWhereFieldPath:fieldPath in:value];
76
+ } else if ([operator isEqualToString:@"ARRAY_CONTAINS_ANY"]) {
77
+ _query = [_query queryWhereFieldPath:fieldPath arrayContainsAny:value];
78
+ } else if ([operator isEqualToString:@"NOT_IN"]) {
79
+ _query = [_query queryWhereFieldPath:fieldPath notIn:value];
80
+ }
81
+ } else if (filter[@"operator"] && filter[@"queries"]) {
82
+ // Filter query
83
+ FIRFilter *generatedFilter = [self _applyFilterQueries:filter];
84
+ _query = [_query queryWhereFilter:generatedFilter];
85
+ } else {
86
+ @throw
87
+ [NSException exceptionWithName:@"InvalidOperator"
88
+ reason:@"The correct signature for a filter has not been parsed"
89
+ userInfo:nil];
90
+ }
91
+ }
92
+ }
93
+
94
+ - (FIRFilter *)_applyFilterQueries:(NSDictionary<NSString *, id> *)map {
95
+ if ([map objectForKey:@"fieldPath"]) {
96
+ NSString *operator= map[@"operator"];
97
+ NSArray *fieldPathArray = map[@"fieldPath"][@"_segments"];
98
+
55
99
  FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathArray];
56
- NSString *operator= filter[@"operator"];
57
- id value = [RNFBFirestoreSerialize parseTypeMap:_firestore typeMap:filter[@"value"]];
100
+ id value = [RNFBFirestoreSerialize parseTypeMap:_firestore typeMap:map[@"value"]];
58
101
 
59
102
  if ([operator isEqualToString:@"EQUAL"]) {
60
- _query = [_query queryWhereFieldPath:fieldPath isEqualTo:value];
103
+ return [FIRFilter filterWhereFieldPath:fieldPath isEqualTo:value];
61
104
  } else if ([operator isEqualToString:@"NOT_EQUAL"]) {
62
- _query = [_query queryWhereFieldPath:fieldPath isNotEqualTo:value];
63
- } else if ([operator isEqualToString:@"GREATER_THAN"]) {
64
- _query = [_query queryWhereFieldPath:fieldPath isGreaterThan:value];
65
- } else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
66
- _query = [_query queryWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value];
105
+ return [FIRFilter filterWhereFieldPath:fieldPath isNotEqualTo:value];
67
106
  } else if ([operator isEqualToString:@"LESS_THAN"]) {
68
- _query = [_query queryWhereFieldPath:fieldPath isLessThan:value];
107
+ return [FIRFilter filterWhereFieldPath:fieldPath isLessThan:value];
69
108
  } else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
70
- _query = [_query queryWhereFieldPath:fieldPath isLessThanOrEqualTo:value];
109
+ return [FIRFilter filterWhereFieldPath:fieldPath isLessThanOrEqualTo:value];
110
+ } else if ([operator isEqualToString:@"GREATER_THAN"]) {
111
+ return [FIRFilter filterWhereFieldPath:fieldPath isGreaterThan:value];
112
+ } else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
113
+ return [FIRFilter filterWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value];
71
114
  } else if ([operator isEqualToString:@"ARRAY_CONTAINS"]) {
72
- _query = [_query queryWhereFieldPath:fieldPath arrayContains:value];
73
- } else if ([operator isEqualToString:@"IN"]) {
74
- _query = [_query queryWhereFieldPath:fieldPath in:value];
115
+ return [FIRFilter filterWhereFieldPath:fieldPath arrayContains:value];
75
116
  } else if ([operator isEqualToString:@"ARRAY_CONTAINS_ANY"]) {
76
- _query = [_query queryWhereFieldPath:fieldPath arrayContainsAny:value];
117
+ return [FIRFilter filterWhereFieldPath:fieldPath arrayContainsAny:value];
118
+ } else if ([operator isEqualToString:@"IN"]) {
119
+ return [FIRFilter filterWhereFieldPath:fieldPath in:value];
77
120
  } else if ([operator isEqualToString:@"NOT_IN"]) {
78
- _query = [_query queryWhereFieldPath:fieldPath notIn:value];
121
+ return [FIRFilter filterWhereFieldPath:fieldPath notIn:value];
122
+ } else {
123
+ @throw [NSException exceptionWithName:@"InvalidOperator"
124
+ reason:@"Invalid operator"
125
+ userInfo:nil];
79
126
  }
80
127
  }
128
+
129
+ NSString *op = map[@"operator"];
130
+ NSArray<NSDictionary<NSString *, id> *> *queries = map[@"queries"];
131
+ NSMutableArray<FIRFilter *> *parsedFilters = [NSMutableArray array];
132
+
133
+ for (NSDictionary *query in queries) {
134
+ [parsedFilters addObject:[self _applyFilterQueries:query]];
135
+ }
136
+
137
+ if ([op isEqual:@"AND"]) {
138
+ return [FIRFilter andFilterWithFilters:parsedFilters];
139
+ }
140
+
141
+ if ([op isEqualToString:@"OR"]) {
142
+ return [FIRFilter orFilterWithFilters:parsedFilters];
143
+ }
144
+
145
+ @throw [NSException exceptionWithName:@"InvalidOperator" reason:@"Invalid operator" userInfo:nil];
81
146
  }
82
147
 
83
148
  - (void)applyOrders {
@@ -0,0 +1,151 @@
1
+ import { isString, isNull, isUndefined, isArray } from '@react-native-firebase/app/lib/common';
2
+ import { fromDotSeparatedString } from './FirestoreFieldPath';
3
+ import { generateNativeData } from './utils/serialize';
4
+ import { OPERATORS } from './FirestoreQueryModifiers';
5
+ const AND_QUERY = 'AND';
6
+ const OR_QUERY = 'OR';
7
+
8
+ export function Filter(fieldPath, operator, value) {
9
+ return new _Filter(fieldPath, operator, value);
10
+ }
11
+
12
+ export function _Filter(fieldPath, operator, value, filterOperator, queries) {
13
+ if ([AND_QUERY, OR_QUERY].includes(filterOperator)) {
14
+ // AND or OR Filter (list of Filters)
15
+ this.operator = filterOperator;
16
+ this.queries = queries;
17
+
18
+ this._toMap = function _toMap() {
19
+ return {
20
+ operator: this.operator,
21
+ queries: this.queries.map(query => query._toMap()),
22
+ };
23
+ };
24
+
25
+ return this;
26
+ } else {
27
+ // Filter
28
+ this.fieldPath = fieldPath;
29
+ this.operator = operator;
30
+ this.value = value;
31
+
32
+ this._toMap = function _toMap() {
33
+ return {
34
+ fieldPath: this.fieldPath,
35
+ operator: this.operator,
36
+ value: this.value,
37
+ };
38
+ };
39
+
40
+ return this;
41
+ }
42
+ }
43
+
44
+ Filter.and = function and(...queries) {
45
+ if (queries.length > 10 || queries.length < 2) {
46
+ throw new Error(`Expected 2-10 instances of Filter, but got ${queries.length} Filters`);
47
+ }
48
+
49
+ const validateFilters = queries.every(filter => filter instanceof _Filter);
50
+
51
+ if (!validateFilters) {
52
+ throw new Error('Expected every argument to be an instance of Filter');
53
+ }
54
+
55
+ return new _Filter(null, null, null, AND_QUERY, queries);
56
+ };
57
+
58
+ function hasOrOperator(obj) {
59
+ return obj.operator === 'OR' || (Array.isArray(obj.queries) && obj.queries.some(hasOrOperator));
60
+ }
61
+
62
+ Filter.or = function or(...queries) {
63
+ if (queries.length > 10 || queries.length < 2) {
64
+ throw new Error(`Expected 2-10 instances of Filter, but got ${queries.length} Filters`);
65
+ }
66
+
67
+ const validateFilters = queries.every(filter => filter instanceof _Filter);
68
+
69
+ if (!validateFilters) {
70
+ throw new Error('Expected every argument to be an instance of Filter');
71
+ }
72
+
73
+ const hasOr = queries.some(hasOrOperator);
74
+
75
+ if (hasOr) {
76
+ throw new Error('OR Filters with nested OR Filters are not supported');
77
+ }
78
+
79
+ return new _Filter(null, null, null, OR_QUERY, queries);
80
+ };
81
+
82
+ function mapFieldQuery({ fieldPath, operator, value, queries }, modifiers) {
83
+ if (operator === AND_QUERY || operator === OR_QUERY) {
84
+ return {
85
+ operator,
86
+ queries: queries.map(filter => mapFieldQuery(filter, modifiers)),
87
+ };
88
+ }
89
+
90
+ let path;
91
+ if (isString(fieldPath)) {
92
+ try {
93
+ path = fromDotSeparatedString(fieldPath);
94
+ } catch (e) {
95
+ throw new Error(`first argument of Filter(*,_ , _) 'fieldPath' ${e.message}.`);
96
+ }
97
+ } else {
98
+ path = fieldPath;
99
+ }
100
+
101
+ if (!modifiers.isValidOperator(operator)) {
102
+ throw new Error(
103
+ "second argument of Filter(*,_ , _) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.",
104
+ );
105
+ }
106
+
107
+ if (isUndefined(value)) {
108
+ throw new Error("third argument of Filter(*,_ , _) 'value' argument expected.");
109
+ }
110
+
111
+ if (
112
+ isNull(value) &&
113
+ !modifiers.isEqualOperator(operator) &&
114
+ !modifiers.isNotEqualOperator(operator)
115
+ ) {
116
+ throw new Error(
117
+ "third argument of Filter(*,_ , _) 'value' is invalid. You can only perform equals comparisons on null",
118
+ );
119
+ }
120
+
121
+ if (modifiers.isInOperator(operator)) {
122
+ if (!isArray(value) || !value.length) {
123
+ throw new Error(
124
+ `third argument of Filter(*,_ , _) 'value' is invalid. A non-empty array is required for '${operator}' filters.`,
125
+ );
126
+ }
127
+
128
+ if (value.length > 10) {
129
+ throw new Error(
130
+ `third argument of Filter(*,_ , _) 'value' is invalid. '${operator}' filters support a maximum of 10 elements in the value array.`,
131
+ );
132
+ }
133
+ }
134
+
135
+ return {
136
+ fieldPath: path,
137
+ operator: OPERATORS[operator],
138
+ value: generateNativeData(value, true),
139
+ };
140
+ }
141
+
142
+ export function generateFilters(filter, modifiers) {
143
+ const filterMap = filter._toMap();
144
+
145
+ const queriesMaps = filterMap.queries.map(filter => mapFieldQuery(filter, modifiers));
146
+
147
+ return {
148
+ operator: filterMap.operator,
149
+ queries: queriesMaps,
150
+ };
151
+ }
@@ -28,6 +28,7 @@ import FirestoreDocumentSnapshot from './FirestoreDocumentSnapshot';
28
28
  import FirestoreFieldPath, { fromDotSeparatedString } from './FirestoreFieldPath';
29
29
  import FirestoreQuerySnapshot from './FirestoreQuerySnapshot';
30
30
  import { parseSnapshotArgs } from './utils';
31
+ import { _Filter, generateFilters } from './FirestoreFilter';
31
32
 
32
33
  let _id = 0;
33
34
 
@@ -406,62 +407,79 @@ export default class FirestoreQuery {
406
407
  );
407
408
  }
408
409
 
409
- where(fieldPath, opStr, value) {
410
- if (!isString(fieldPath) && !(fieldPath instanceof FirestoreFieldPath)) {
410
+ where(fieldPathOrFilter, opStr, value) {
411
+ if (
412
+ !isString(fieldPathOrFilter) &&
413
+ !(fieldPathOrFilter instanceof FirestoreFieldPath) &&
414
+ !(fieldPathOrFilter instanceof _Filter)
415
+ ) {
411
416
  throw new Error(
412
- "firebase.firestore().collection().where(*) 'fieldPath' must be a string or instance of FieldPath.",
417
+ "firebase.firestore().collection().where(*) 'fieldPath' must be a string, instance of FieldPath or instance of Filter.",
413
418
  );
414
419
  }
415
420
 
416
- let path;
417
-
418
- if (isString(fieldPath)) {
419
- try {
420
- path = fromDotSeparatedString(fieldPath);
421
- } catch (e) {
422
- throw new Error(`firebase.firestore().collection().where(*) 'fieldPath' ${e.message}.`);
423
- }
421
+ let modifiers;
422
+ if (fieldPathOrFilter instanceof _Filter && fieldPathOrFilter.queries) {
423
+ //AND or OR filter
424
+ const filters = generateFilters(fieldPathOrFilter, this._modifiers);
425
+ modifiers = this._modifiers._copy().filterWhere(filters);
424
426
  } else {
425
- path = fieldPath;
426
- }
427
-
428
- if (!this._modifiers.isValidOperator(opStr)) {
429
- throw new Error(
430
- "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.",
431
- );
432
- }
427
+ if (fieldPathOrFilter instanceof _Filter) {
428
+ // Standard Filter. Usual path.
429
+ opStr = fieldPathOrFilter.operator;
430
+ value = fieldPathOrFilter.value;
431
+ fieldPathOrFilter = fieldPathOrFilter.fieldPath;
432
+ }
433
+ let path;
433
434
 
434
- if (isUndefined(value)) {
435
- throw new Error(
436
- "firebase.firestore().collection().where(_, _, *) 'value' argument expected.",
437
- );
438
- }
435
+ if (isString(fieldPathOrFilter)) {
436
+ try {
437
+ path = fromDotSeparatedString(fieldPathOrFilter);
438
+ } catch (e) {
439
+ throw new Error(`firebase.firestore().collection().where(*) 'fieldPath' ${e.message}.`);
440
+ }
441
+ } else {
442
+ path = fieldPathOrFilter;
443
+ }
439
444
 
440
- if (
441
- isNull(value) &&
442
- !this._modifiers.isEqualOperator(opStr) &&
443
- !this._modifiers.isNotEqualOperator(opStr)
444
- ) {
445
- throw new Error(
446
- "firebase.firestore().collection().where(_, _, *) 'value' is invalid. You can only perform equals comparisons on null",
447
- );
448
- }
445
+ if (!this._modifiers.isValidOperator(opStr)) {
446
+ throw new Error(
447
+ "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.",
448
+ );
449
+ }
449
450
 
450
- if (this._modifiers.isInOperator(opStr)) {
451
- if (!isArray(value) || !value.length) {
451
+ if (isUndefined(value)) {
452
452
  throw new Error(
453
- `firebase.firestore().collection().where(_, _, *) 'value' is invalid. A non-empty array is required for '${opStr}' filters.`,
453
+ "firebase.firestore().collection().where(_, _, *) 'value' argument expected.",
454
454
  );
455
455
  }
456
456
 
457
- if (value.length > 10) {
457
+ if (
458
+ isNull(value) &&
459
+ !this._modifiers.isEqualOperator(opStr) &&
460
+ !this._modifiers.isNotEqualOperator(opStr)
461
+ ) {
458
462
  throw new Error(
459
- `firebase.firestore().collection().where(_, _, *) 'value' is invalid. '${opStr}' filters support a maximum of 10 elements in the value array.`,
463
+ "firebase.firestore().collection().where(_, _, *) 'value' is invalid. You can only perform equals comparisons on null",
460
464
  );
461
465
  }
462
- }
463
466
 
464
- const modifiers = this._modifiers._copy().where(path, opStr, value);
467
+ if (this._modifiers.isInOperator(opStr)) {
468
+ if (!isArray(value) || !value.length) {
469
+ throw new Error(
470
+ `firebase.firestore().collection().where(_, _, *) 'value' is invalid. A non-empty array is required for '${opStr}' filters.`,
471
+ );
472
+ }
473
+
474
+ if (value.length > 10) {
475
+ throw new Error(
476
+ `firebase.firestore().collection().where(_, _, *) 'value' is invalid. '${opStr}' filters support a maximum of 10 elements in the value array.`,
477
+ );
478
+ }
479
+ }
480
+
481
+ modifiers = this._modifiers._copy().where(path, opStr, value);
482
+ }
465
483
 
466
484
  try {
467
485
  modifiers.validateWhere();
@@ -19,7 +19,7 @@ import { isNumber } from '@react-native-firebase/app/lib/common';
19
19
  import FirestoreFieldPath, { DOCUMENT_ID } from './FirestoreFieldPath';
20
20
  import { buildNativeArray, generateNativeData } from './utils/serialize';
21
21
 
22
- const OPERATORS = {
22
+ export const OPERATORS = {
23
23
  '==': 'EQUAL',
24
24
  '>': 'GREATER_THAN',
25
25
  '>=': 'GREATER_THAN_OR_EQUAL',
@@ -57,6 +57,14 @@ export default class FirestoreQueryModifiers {
57
57
  this._startAfter = undefined;
58
58
  this._endAt = undefined;
59
59
  this._endBefore = undefined;
60
+
61
+ // Pulled out of function to preserve their state
62
+ this.hasInequality = false;
63
+ this.hasNotEqual = false;
64
+ this.hasArrayContains = false;
65
+ this.hasArrayContainsAny = false;
66
+ this.hasIn = false;
67
+ this.hasNotIn = false;
60
68
  }
61
69
 
62
70
  _copy() {
@@ -221,118 +229,129 @@ export default class FirestoreQueryModifiers {
221
229
  return this;
222
230
  }
223
231
 
232
+ filterWhere(filter) {
233
+ this._filters = this._filters.concat(filter);
234
+ return this;
235
+ }
236
+
224
237
  validateWhere() {
225
- let hasInequality;
226
- let hasNotEqual;
238
+ if (this._filters.length > 0) {
239
+ this._filterCheck(this._filters);
240
+ }
241
+ }
242
+
243
+ _filterCheck(filters) {
244
+ for (let i = 0; i < filters.length; i++) {
245
+ const filter = filters[i];
246
+
247
+ if (filter.queries) {
248
+ // Recursively check sub-queries for Filters
249
+ this._filterCheck(filter.queries);
250
+ // If it is a Filter query, skip the rest of the loop
251
+ continue;
252
+ }
227
253
 
228
- for (let i = 0; i < this._filters.length; i++) {
229
- const filter = this._filters[i];
230
254
  // Skip if no inequality
231
255
  if (!INEQUALITY[filter.operator]) {
232
256
  continue;
233
257
  }
234
258
 
235
259
  if (filter.operator === OPERATORS['!=']) {
236
- if (hasNotEqual) {
260
+ if (this.hasNotEqual) {
237
261
  throw new Error("Invalid query. You cannot use more than one '!=' inequality filter.");
238
262
  }
239
263
  //needs to set hasNotEqual = true before setting first hasInequality = filter. It is used in a condition check later
240
- hasNotEqual = true;
264
+ this.hasNotEqual = true;
241
265
  }
242
266
 
243
267
  // Set the first inequality
244
- if (!hasInequality) {
245
- hasInequality = filter;
268
+ if (!this.hasInequality) {
269
+ this.hasInequality = filter;
246
270
  continue;
247
271
  }
248
272
 
249
273
  // Check the set value is the same as the new one
250
- if (INEQUALITY[filter.operator] && hasInequality) {
251
- if (hasInequality.fieldPath._toPath() !== filter.fieldPath._toPath()) {
274
+ if (INEQUALITY[filter.operator] && this.hasInequality) {
275
+ if (this.hasInequality.fieldPath._toPath() !== filter.fieldPath._toPath()) {
252
276
  throw new Error(
253
- `Invalid query. All where filters with an inequality (<, <=, >, != or >=) must be on the same field. But you have inequality filters on '${hasInequality.fieldPath._toPath()}' and '${filter.fieldPath._toPath()}'`,
277
+ `Invalid query. All where filters with an inequality (<, <=, >, != or >=) must be on the same field. But you have inequality filters on '${this.hasInequality.fieldPath._toPath()}' and '${filter.fieldPath._toPath()}'`,
254
278
  );
255
279
  }
256
280
  }
257
281
  }
258
282
 
259
- let hasArrayContains;
260
- let hasArrayContainsAny;
261
- let hasIn;
262
- let hasNotIn;
263
-
264
- for (let i = 0; i < this._filters.length; i++) {
265
- const filter = this._filters[i];
283
+ for (let i = 0; i < filters.length; i++) {
284
+ const filter = filters[i];
266
285
 
267
286
  if (filter.operator === OPERATORS['array-contains']) {
268
- if (hasArrayContains) {
287
+ if (this.hasArrayContains) {
269
288
  throw new Error('Invalid query. Queries only support a single array-contains filter.');
270
289
  }
271
- hasArrayContains = true;
290
+ this.hasArrayContains = true;
272
291
  }
273
292
 
274
293
  if (filter.operator === OPERATORS['array-contains-any']) {
275
- if (hasArrayContainsAny) {
294
+ if (this.hasArrayContainsAny) {
276
295
  throw new Error(
277
296
  "Invalid query. You cannot use more than one 'array-contains-any' filter.",
278
297
  );
279
298
  }
280
299
 
281
- if (hasIn) {
300
+ if (this.hasIn) {
282
301
  throw new Error(
283
302
  "Invalid query. You cannot use 'array-contains-any' filters with 'in' filters.",
284
303
  );
285
304
  }
286
305
 
287
- if (hasNotIn) {
306
+ if (this.hasNotIn) {
288
307
  throw new Error(
289
308
  "Invalid query. You cannot use 'array-contains-any' filters with 'not-in' filters.",
290
309
  );
291
310
  }
292
311
 
293
- hasArrayContainsAny = true;
312
+ this.hasArrayContainsAny = true;
294
313
  }
295
314
 
296
315
  if (filter.operator === OPERATORS.in) {
297
- if (hasIn) {
316
+ if (this.hasIn) {
298
317
  throw new Error("Invalid query. You cannot use more than one 'in' filter.");
299
318
  }
300
319
 
301
- if (hasArrayContainsAny) {
320
+ if (this.hasArrayContainsAny) {
302
321
  throw new Error(
303
322
  "Invalid query. You cannot use 'in' filters with 'array-contains-any' filters.",
304
323
  );
305
324
  }
306
325
 
307
- if (hasNotIn) {
326
+ if (this.hasNotIn) {
308
327
  throw new Error("Invalid query. You cannot use 'in' filters with 'not-in' filters.");
309
328
  }
310
329
 
311
- hasIn = true;
330
+ this.hasIn = true;
312
331
  }
313
332
 
314
333
  if (filter.operator === OPERATORS['not-in']) {
315
- if (hasNotIn) {
334
+ if (this.hasNotIn) {
316
335
  throw new Error("Invalid query. You cannot use more than one 'not-in' filter.");
317
336
  }
318
337
 
319
- if (hasNotEqual) {
338
+ if (this.hasNotEqual) {
320
339
  throw new Error(
321
340
  "Invalid query. You cannot use 'not-in' filters with '!=' inequality filters",
322
341
  );
323
342
  }
324
343
 
325
- if (hasIn) {
344
+ if (this.hasIn) {
326
345
  throw new Error("Invalid query. You cannot use 'not-in' filters with 'in' filters.");
327
346
  }
328
347
 
329
- if (hasArrayContainsAny) {
348
+ if (this.hasArrayContainsAny) {
330
349
  throw new Error(
331
350
  "Invalid query. You cannot use 'not-in' filters with 'array-contains-any' filters.",
332
351
  );
333
352
  }
334
353
 
335
- hasNotIn = true;
354
+ this.hasNotIn = true;
336
355
  }
337
356
  }
338
357
  }
@@ -356,6 +375,10 @@ export default class FirestoreQueryModifiers {
356
375
  }
357
376
 
358
377
  validateOrderBy() {
378
+ this._validateOrderByCheck(this._filters);
379
+ }
380
+
381
+ _validateOrderByCheck(filters) {
359
382
  // Ensure order hasn't been called on the same field
360
383
  if (this._orders.length > 1) {
361
384
  const orders = this._orders.map($ => $.fieldPath._toPath());
@@ -367,13 +390,20 @@ export default class FirestoreQueryModifiers {
367
390
  }
368
391
 
369
392
  // Skip if no where filters
370
- if (this._filters.length === 0) {
393
+ if (filters.length === 0) {
371
394
  return;
372
395
  }
373
396
 
374
397
  // Ensure the first order field path is equal to the inequality filter field path
375
- for (let i = 0; i < this._filters.length; i++) {
376
- const filter = this._filters[i];
398
+ for (let i = 0; i < filters.length; i++) {
399
+ const filter = filters[i];
400
+
401
+ if (filter.queries) {
402
+ // Recursively check sub-queries for Filters
403
+ this._validateOrderByCheck(filter.queries);
404
+ // If it is a Filter query, skip the rest of the loop
405
+ continue;
406
+ }
377
407
  const filterFieldPath = filter.fieldPath._toPath();
378
408
 
379
409
  for (let k = 0; k < this._orders.length; k++) {
@@ -21,13 +21,14 @@ import FirestoreFieldPath from './FirestoreFieldPath';
21
21
  import FirestoreFieldValue from './FirestoreFieldValue';
22
22
  import FirestoreGeoPoint from './FirestoreGeoPoint';
23
23
  import FirestoreTimestamp from './FirestoreTimestamp';
24
-
24
+ import { Filter } from './FirestoreFilter';
25
25
  export default {
26
26
  Blob: FirestoreBlob,
27
27
  FieldPath: FirestoreFieldPath,
28
28
  FieldValue: FirestoreFieldValue,
29
29
  GeoPoint: FirestoreGeoPoint,
30
30
  Timestamp: FirestoreTimestamp,
31
+ Filter: Filter,
31
32
 
32
33
  CACHE_SIZE_UNLIMITED: -1,
33
34
 
package/lib/index.d.ts CHANGED
@@ -49,6 +49,47 @@ import { ReactNativeFirebase } from '@react-native-firebase/app';
49
49
  */
50
50
  export namespace FirebaseFirestoreTypes {
51
51
  import FirebaseModule = ReactNativeFirebase.FirebaseModule;
52
+ /**
53
+ * An instance of Filter used to generate Firestore Filter queries.
54
+ */
55
+
56
+ export type QueryFilterType = 'OR' | 'AND';
57
+
58
+ export interface QueryFilterConstraint {
59
+ fieldPath: keyof T | FieldPath;
60
+ operator: WhereFilterOp;
61
+ value: any;
62
+ }
63
+
64
+ export interface QueryCompositeFilterConstraint {
65
+ operator: QueryFilterType;
66
+ queries: QueryFilterConstraint[];
67
+ }
68
+ /**
69
+ * The Filter functions used to generate an instance of Filter.
70
+ */
71
+ export interface FilterFunction {
72
+ /**
73
+ * The Filter function used to generate an instance of Filter.
74
+ * e.g. Filter('name', '==', 'Ada')
75
+ */
76
+ (fieldPath: keyof T | FieldPath, operator: WhereFilterOp, value: any): QueryFilterConstraint;
77
+ /**
78
+ * The Filter.or() static function used to generate a logical OR query using multiple Filter instances.
79
+ * e.g. Filter.or(Filter('name', '==', 'Ada'), Filter('name', '==', 'Bob'))
80
+ */
81
+ or(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint;
82
+ /**
83
+ * The Filter.and() static function used to generate a logical AND query using multiple Filter instances.
84
+ * e.g. Filter.and(Filter('name', '==', 'Ada'), Filter('name', '==', 'Bob'))
85
+ */
86
+ and(...queries: QueryFilterConstraint[]): QueryCompositeFilterConstraint;
87
+ }
88
+ /**
89
+ * The Filter function used to generate an instance of Filter.
90
+ * e.g. Filter('name', '==', 'Ada')
91
+ */
92
+ export const Filter: FilterFunction;
52
93
 
53
94
  /**
54
95
  * An immutable object representing an array of bytes.
package/lib/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- module.exports = '17.4.3';
2
+ module.exports = '17.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-firebase/firestore",
3
- "version": "17.4.3",
3
+ "version": "17.5.0",
4
4
  "author": "Invertase <oss@invertase.io> (http://invertase.io)",
5
5
  "description": "React Native Firebase - Cloud Firestore is a NoSQL cloud database to store and sync data between your React Native application and Firebase's database. The API matches the Firebase Web SDK whilst taking advantage of the native SDKs performance and offline capabilities.",
6
6
  "main": "lib/index.js",
@@ -27,10 +27,10 @@
27
27
  "firestore"
28
28
  ],
29
29
  "peerDependencies": {
30
- "@react-native-firebase/app": "17.4.3"
30
+ "@react-native-firebase/app": "17.5.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "gitHead": "3c47228dcb65ee6f9ac71f30c4dffbe4cf088d2a"
35
+ "gitHead": "a58ea4bfd72c903d43844da30d4f3bf5feb0f057"
36
36
  }