@react-native-firebase/firestore 17.4.3 → 18.0.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,33 @@
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
+ ## [18.0.0](https://github.com/invertase/react-native-firebase/compare/v17.5.0...v18.0.0) (2023-06-05)
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ - **app, sdk:** this version of the underlying firebase-ios-sdk has
11
+ a minimum Xcode requirement of 14.1 which transitively implies a macOS
12
+ minimum version of 12.5
13
+ - **app, sdk:** the "safetyNet" provider for App Check has been removed
14
+ from the underlying firebase-android-sdk and we have removed it here. You
15
+ should upgrade to the "playIntegrity" provider for App Check
16
+
17
+ ### Features
18
+
19
+ - **app, sdk:** android-sdk v32 - app-check safetyNet provider is removed ([a0e76ec](https://github.com/invertase/react-native-firebase/commit/a0e76ecab65c69a19055a84bc19c069482b1bc88))
20
+ - **app, sdk:** ios-sdk 10.10.0, requires Xcode 14.1+ / macOS 12.5+ ([3122918](https://github.com/invertase/react-native-firebase/commit/3122918c19c27696caf51f30caafdcaa76807db8))
21
+
22
+ ### Bug Fixes
23
+
24
+ - **firestore, types:** add types for Filter constraints on Queries ([#7124](https://github.com/invertase/react-native-firebase/issues/7124)) ([0785d27](https://github.com/invertase/react-native-firebase/commit/0785d276669b9c875951d4527e8884c9014e48fe))
25
+ - **firestore:** Allow queries with combined in and array-contains-any ([#7142](https://github.com/invertase/react-native-firebase/issues/7142)) ([8da6951](https://github.com/invertase/react-native-firebase/commit/8da69519b3dd516a67976a490434f8f9c12a426e))
26
+
27
+ ## [17.5.0](https://github.com/invertase/react-native-firebase/compare/v17.4.3...v17.5.0) (2023-05-11)
28
+
29
+ ### Features
30
+
31
+ - **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))
32
+
6
33
  ### [17.4.3](https://github.com/invertase/react-native-firebase/compare/v17.4.2...v17.4.3) (2023-04-26)
7
34
 
8
35
  **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,113 @@ 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) {
282
- throw new Error(
283
- "Invalid query. You cannot use 'array-contains-any' filters with 'in' filters.",
284
- );
285
- }
286
-
287
- if (hasNotIn) {
300
+ if (this.hasNotIn) {
288
301
  throw new Error(
289
302
  "Invalid query. You cannot use 'array-contains-any' filters with 'not-in' filters.",
290
303
  );
291
304
  }
292
305
 
293
- hasArrayContainsAny = true;
306
+ this.hasArrayContainsAny = true;
294
307
  }
295
308
 
296
309
  if (filter.operator === OPERATORS.in) {
297
- if (hasIn) {
298
- throw new Error("Invalid query. You cannot use more than one 'in' filter.");
299
- }
300
-
301
- if (hasArrayContainsAny) {
302
- throw new Error(
303
- "Invalid query. You cannot use 'in' filters with 'array-contains-any' filters.",
304
- );
305
- }
306
-
307
- if (hasNotIn) {
310
+ if (this.hasNotIn) {
308
311
  throw new Error("Invalid query. You cannot use 'in' filters with 'not-in' filters.");
309
312
  }
310
313
 
311
- hasIn = true;
314
+ this.hasIn = true;
312
315
  }
313
316
 
314
317
  if (filter.operator === OPERATORS['not-in']) {
315
- if (hasNotIn) {
318
+ if (this.hasNotIn) {
316
319
  throw new Error("Invalid query. You cannot use more than one 'not-in' filter.");
317
320
  }
318
321
 
319
- if (hasNotEqual) {
322
+ if (this.hasNotEqual) {
320
323
  throw new Error(
321
324
  "Invalid query. You cannot use 'not-in' filters with '!=' inequality filters",
322
325
  );
323
326
  }
324
327
 
325
- if (hasIn) {
328
+ if (this.hasIn) {
326
329
  throw new Error("Invalid query. You cannot use 'not-in' filters with 'in' filters.");
327
330
  }
328
331
 
329
- if (hasArrayContainsAny) {
332
+ if (this.hasArrayContainsAny) {
330
333
  throw new Error(
331
334
  "Invalid query. You cannot use 'not-in' filters with 'array-contains-any' filters.",
332
335
  );
333
336
  }
334
337
 
335
- hasNotIn = true;
338
+ this.hasNotIn = true;
336
339
  }
337
340
  }
338
341
  }
@@ -356,6 +359,10 @@ export default class FirestoreQueryModifiers {
356
359
  }
357
360
 
358
361
  validateOrderBy() {
362
+ this._validateOrderByCheck(this._filters);
363
+ }
364
+
365
+ _validateOrderByCheck(filters) {
359
366
  // Ensure order hasn't been called on the same field
360
367
  if (this._orders.length > 1) {
361
368
  const orders = this._orders.map($ => $.fieldPath._toPath());
@@ -367,13 +374,20 @@ export default class FirestoreQueryModifiers {
367
374
  }
368
375
 
369
376
  // Skip if no where filters
370
- if (this._filters.length === 0) {
377
+ if (filters.length === 0) {
371
378
  return;
372
379
  }
373
380
 
374
381
  // 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];
382
+ for (let i = 0; i < filters.length; i++) {
383
+ const filter = filters[i];
384
+
385
+ if (filter.queries) {
386
+ // Recursively check sub-queries for Filters
387
+ this._validateOrderByCheck(filter.queries);
388
+ // If it is a Filter query, skip the rest of the loop
389
+ continue;
390
+ }
377
391
  const filterFieldPath = filter.fieldPath._toPath();
378
392
 
379
393
  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.
@@ -1354,6 +1395,24 @@ export namespace FirebaseFirestoreTypes {
1354
1395
  * @param value The comparison value.
1355
1396
  */
1356
1397
  where(fieldPath: keyof T | FieldPath, opStr: WhereFilterOp, value: any): Query<T>;
1398
+
1399
+ /**
1400
+ * Creates and returns a new Query with the additional filter that documents must contain the specified field and
1401
+ * the value should satisfy the relation constraint provided.
1402
+ *
1403
+ * #### Example
1404
+ *
1405
+ * ```js
1406
+ * // Get all users who's age is 30 or above
1407
+ * const querySnapshot = await firebase.firestore()
1408
+ * .collection('users')
1409
+ * .where(Filter('age', '>=', 30));
1410
+ * .get();
1411
+ * ```
1412
+ *
1413
+ * @param filter The filter to apply to the query.
1414
+ */
1415
+ where(filter: QueryFilterConstraint | QueryCompositeFilterConstraint): Query<T>;
1357
1416
  }
1358
1417
 
1359
1418
  /**
@@ -2006,6 +2065,11 @@ export namespace FirebaseFirestoreTypes {
2006
2065
  */
2007
2066
  Timestamp: typeof Timestamp;
2008
2067
 
2068
+ /**
2069
+ * Returns the `Filter` function.
2070
+ */
2071
+ Filter: typeof Filter;
2072
+
2009
2073
  /**
2010
2074
  * Used to set the cache size to unlimited when passing to `cacheSizeBytes` in
2011
2075
  * `firebase.firestore().settings()`.
@@ -2276,6 +2340,8 @@ export const firebase: ReactNativeFirebase.Module & {
2276
2340
  ): ReactNativeFirebase.FirebaseApp & { firestore(): FirebaseFirestoreTypes.Module };
2277
2341
  };
2278
2342
 
2343
+ export const Filter: FilterFunction;
2344
+
2279
2345
  export default defaultExport;
2280
2346
 
2281
2347
  /**
package/lib/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- module.exports = '17.4.3';
2
+ module.exports = '18.0.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": "18.0.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": "18.0.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "gitHead": "3c47228dcb65ee6f9ac71f30c4dffbe4cf088d2a"
35
+ "gitHead": "db49dfeab62fa0c52530ccf2bfdfe9e27947bdbd"
36
36
  }