Skip to content

Commit f3516ae

Browse files
feat(firestore): query operators: 'not-in' & '!=' (invertase#4474)
* feat(firestore): query operators: 'not-in' & '!=' * chore(firestore): update filters * test(firestore): not-in, != e2e tests * chore(ios): pods version updates * format: code format * chore(firestore): spelling & grammar * chore(firestore): PR feedback
1 parent 76a06ac commit f3516ae

File tree

6 files changed

+201
-5
lines changed

6 files changed

+201
-5
lines changed

android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ private void applyFilters(ReadableArray filters) {
7474
case "EQUAL":
7575
query = query.whereEqualTo(Objects.requireNonNull(fieldPath), value);
7676
break;
77+
case "NOT_EQUAL":
78+
query = query.whereNotEqualTo(Objects.requireNonNull(fieldPath), value);
79+
break;
7780
case "GREATER_THAN":
7881
query = query.whereGreaterThan(Objects.requireNonNull(fieldPath), Objects.requireNonNull(value));
7982
break;
@@ -95,6 +98,9 @@ private void applyFilters(ReadableArray filters) {
9598
case "IN":
9699
query = query.whereIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
97100
break;
101+
case "NOT_IN":
102+
query = query.whereNotIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
103+
break;
98104
}
99105
}
100106
}

e2e/Query/where.e2e.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,4 +436,137 @@ describe('firestore().collection().where()', () => {
436436

437437
items.length.should.equal(1);
438438
});
439+
440+
it("should correctly retrieve data when using 'not-in' operator", async () => {
441+
const ref = firebase.firestore().collection(COLLECTION);
442+
443+
await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]);
444+
445+
const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get();
446+
should(result.docs.length).equal(1);
447+
should(result.docs[0].data().notIn).equal('now');
448+
});
449+
450+
it("should throw error when using 'not-in' operator twice", async () => {
451+
const ref = firebase.firestore().collection(COLLECTION);
452+
453+
try {
454+
ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]);
455+
return Promise.reject(new Error('Did not throw an Error.'));
456+
} catch (error) {
457+
error.message.should.containEql("You cannot use more than one 'not-in' filter.");
458+
return Promise.resolve();
459+
}
460+
});
461+
462+
it("should throw error when combining 'not-in' operator with '!=' operator", async () => {
463+
const ref = firebase.firestore().collection(COLLECTION);
464+
465+
try {
466+
ref.where('test', '!=', 1).where('test', 'not-in', [1]);
467+
return Promise.reject(new Error('Did not throw an Error.'));
468+
} catch (error) {
469+
error.message.should.containEql(
470+
"You cannot use 'not-in' filters with '!=' inequality filters",
471+
);
472+
return Promise.resolve();
473+
}
474+
});
475+
476+
it("should throw error when combining 'not-in' operator with 'in' operator", async () => {
477+
const ref = firebase.firestore().collection(COLLECTION);
478+
479+
try {
480+
ref.where('test', 'in', [2]).where('test', 'not-in', [1]);
481+
return Promise.reject(new Error('Did not throw an Error.'));
482+
} catch (error) {
483+
error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters.");
484+
return Promise.resolve();
485+
}
486+
});
487+
488+
it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async () => {
489+
const ref = firebase.firestore().collection(COLLECTION);
490+
491+
try {
492+
ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]);
493+
return Promise.reject(new Error('Did not throw an Error.'));
494+
} catch (error) {
495+
error.message.should.containEql(
496+
"You cannot use 'not-in' filters with 'array-contains-any' filters.",
497+
);
498+
return Promise.resolve();
499+
}
500+
});
501+
502+
it("should throw error when 'not-in' filter has a list of more than 10 items", async () => {
503+
const ref = firebase.firestore().collection(COLLECTION);
504+
505+
try {
506+
ref.where('test', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
507+
return Promise.reject(new Error('Did not throw an Error.'));
508+
} catch (error) {
509+
error.message.should.containEql(
510+
'filters support a maximum of 10 elements in the value array.',
511+
);
512+
return Promise.resolve();
513+
}
514+
});
515+
516+
it("should correctly retrieve data when using '!=' operator", async () => {
517+
const ref = firebase.firestore().collection(COLLECTION);
518+
519+
await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]);
520+
521+
const result = await ref.where('notEqual', '!=', 'here').get();
522+
523+
should(result.docs.length).equal(1);
524+
should(result.docs[0].data().notEqual).equal('now');
525+
});
526+
527+
it("should throw error when using '!=' operator twice ", async () => {
528+
const ref = firebase.firestore().collection(COLLECTION);
529+
530+
try {
531+
ref.where('test', '!=', 1).where('test', '!=', 2);
532+
return Promise.reject(new Error('Did not throw an Error.'));
533+
} catch (error) {
534+
error.message.should.containEql("You cannot use more than one '!=' inequality filter.");
535+
return Promise.resolve();
536+
}
537+
});
538+
539+
it("should throw error when combining '!=' operator with any other inequality operator on a different field", async () => {
540+
const ref = firebase.firestore().collection(COLLECTION);
541+
542+
try {
543+
ref.where('test', '!=', 1).where('differentField', '>', 1);
544+
return Promise.reject(new Error('Did not throw an Error on >.'));
545+
} catch (error) {
546+
error.message.should.containEql('must be on the same field.');
547+
}
548+
549+
try {
550+
ref.where('test', '!=', 1).where('differentField', '<', 1);
551+
return Promise.reject(new Error('Did not throw an Error on <.'));
552+
} catch (error) {
553+
error.message.should.containEql('must be on the same field.');
554+
}
555+
556+
try {
557+
ref.where('test', '!=', 1).where('differentField', '<=', 1);
558+
return Promise.reject(new Error('Did not throw an Error <=.'));
559+
} catch (error) {
560+
error.message.should.containEql('must be on the same field.');
561+
}
562+
563+
try {
564+
ref.where('test', '!=', 1).where('differentField', '>=', 1);
565+
return Promise.reject(new Error('Did not throw an Error >=.'));
566+
} catch (error) {
567+
error.message.should.containEql('must be on the same field.');
568+
}
569+
570+
return Promise.resolve();
571+
});
439572
});

ios/RNFBFirestore/RNFBFirestoreQuery.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ - (void)applyFilters {
5858

5959
if ([operator isEqualToString:@"EQUAL"]) {
6060
_query = [_query queryWhereFieldPath:fieldPath isEqualTo:value];
61+
} else if ([operator isEqualToString:@"NOT_EQUAL"]) {
62+
_query = [_query queryWhereFieldPath:fieldPath isNotEqualTo:value];
6163
} else if ([operator isEqualToString:@"GREATER_THAN"]) {
6264
_query = [_query queryWhereFieldPath:fieldPath isGreaterThan:value];
6365
} else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
@@ -72,6 +74,8 @@ - (void)applyFilters {
7274
_query = [_query queryWhereFieldPath:fieldPath in:value];
7375
} else if ([operator isEqualToString:@"ARRAY_CONTAINS_ANY"]) {
7476
_query = [_query queryWhereFieldPath:fieldPath arrayContainsAny:value];
77+
} else if ([operator isEqualToString:@"NOT_IN"]) {
78+
_query = [_query queryWhereFieldPath:fieldPath notIn:value];
7579
}
7680
}
7781
}

lib/FirestoreQuery.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export default class FirestoreQuery {
383383

384384
if (!this._modifiers.isValidOperator(opStr)) {
385385
throw new Error(
386-
"firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', 'array-contains', 'array-contains-any' or 'in'.",
386+
"firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.",
387387
);
388388
}
389389

lib/FirestoreQueryModifiers.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ const OPERATORS = {
2525
'>=': 'GREATER_THAN_OR_EQUAL',
2626
'<': 'LESS_THAN',
2727
'<=': 'LESS_THAN_OR_EQUAL',
28+
'!=': 'NOT_EQUAL',
2829
'array-contains': 'ARRAY_CONTAINS',
2930
'array-contains-any': 'ARRAY_CONTAINS_ANY',
31+
'not-in': 'NOT_IN',
3032
in: 'IN',
3133
};
3234

@@ -35,6 +37,7 @@ const INEQUALITY = {
3537
LESS_THAN_OR_EQUAL: true,
3638
GREATER_THAN: true,
3739
GREATER_THAN_OR_EQUAL: true,
40+
NOT_EQUAL: true,
3841
};
3942

4043
const DIRECTIONS = {
@@ -190,7 +193,11 @@ export default class FirestoreQueryModifiers {
190193
}
191194

192195
isInOperator(operator) {
193-
return OPERATORS[operator] === 'IN' || OPERATORS[operator] === 'ARRAY_CONTAINS_ANY';
196+
return (
197+
OPERATORS[operator] === 'IN' ||
198+
OPERATORS[operator] === 'ARRAY_CONTAINS_ANY' ||
199+
OPERATORS[operator] === 'NOT_IN'
200+
);
194201
}
195202

196203
where(fieldPath, opStr, value) {
@@ -206,6 +213,7 @@ export default class FirestoreQueryModifiers {
206213

207214
validateWhere() {
208215
let hasInequality;
216+
let hasNotEqual;
209217

210218
for (let i = 0; i < this._filters.length; i++) {
211219
const filter = this._filters[i];
@@ -214,6 +222,14 @@ export default class FirestoreQueryModifiers {
214222
continue;
215223
}
216224

225+
if (filter.operator === OPERATORS['!=']) {
226+
if (hasNotEqual) {
227+
throw new Error("Invalid query. You cannot use more than one '!=' inequality filter.");
228+
}
229+
//needs to set hasNotEqual = true before setting first hasInequality = filter. It is used in a condition check later
230+
hasNotEqual = true;
231+
}
232+
217233
// Set the first inequality
218234
if (!hasInequality) {
219235
hasInequality = filter;
@@ -224,7 +240,7 @@ export default class FirestoreQueryModifiers {
224240
if (INEQUALITY[filter.operator] && hasInequality) {
225241
if (hasInequality.fieldPath._toPath() !== filter.fieldPath._toPath()) {
226242
throw new Error(
227-
`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()}'`,
243+
`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()}'`,
228244
);
229245
}
230246
}
@@ -233,6 +249,7 @@ export default class FirestoreQueryModifiers {
233249
let hasArrayContains;
234250
let hasArrayContainsAny;
235251
let hasIn;
252+
let hasNotIn;
236253

237254
for (let i = 0; i < this._filters.length; i++) {
238255
const filter = this._filters[i];
@@ -257,6 +274,12 @@ export default class FirestoreQueryModifiers {
257274
);
258275
}
259276

277+
if (hasNotIn) {
278+
throw new Error(
279+
"Invalid query. You cannot use 'array-contains-any' filters with 'not-in' filters.",
280+
);
281+
}
282+
260283
hasArrayContainsAny = true;
261284
}
262285

@@ -271,8 +294,36 @@ export default class FirestoreQueryModifiers {
271294
);
272295
}
273296

297+
if (hasNotIn) {
298+
throw new Error("Invalid query. You cannot use 'in' filters with 'not-in' filters.");
299+
}
300+
274301
hasIn = true;
275302
}
303+
304+
if (filter.operator === OPERATORS['not-in']) {
305+
if (hasNotIn) {
306+
throw new Error("Invalid query. You cannot use more than one 'not-in' filter.");
307+
}
308+
309+
if (hasNotEqual) {
310+
throw new Error(
311+
"Invalid query. You cannot use 'not-in' filters with '!=' inequality filters",
312+
);
313+
}
314+
315+
if (hasIn) {
316+
throw new Error("Invalid query. You cannot use 'not-in' filters with 'in' filters.");
317+
}
318+
319+
if (hasArrayContainsAny) {
320+
throw new Error(
321+
"Invalid query. You cannot use 'not-in' filters with 'array-contains-any' filters.",
322+
);
323+
}
324+
325+
hasNotIn = true;
326+
}
276327
}
277328
}
278329

lib/index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ export namespace FirebaseFirestoreTypes {
12401240
* ```
12411241
*
12421242
* @param fieldPath The path to compare.
1243-
* @param opStr The operation string (e.g "<", "<=", "==", ">", ">=", "array-contains", "array-contains-any", "in").
1243+
* @param opStr The operation string (e.g "<", "<=", "==", ">", ">=", "!=", "array-contains", "array-contains-any", "in", "not-in").
12441244
* @param value The comparison value.
12451245
*/
12461246
where(fieldPath: keyof T | FieldPath, opStr: WhereFilterOp, value: any): Query<T>;
@@ -1255,9 +1255,11 @@ export namespace FirebaseFirestoreTypes {
12551255
| '=='
12561256
| '>'
12571257
| '>='
1258+
| '!='
12581259
| 'array-contains'
12591260
| 'array-contains-any'
1260-
| 'in';
1261+
| 'in'
1262+
| 'not-in';
12611263

12621264
/**
12631265
* A `QuerySnapshot` contains zero or more `QueryDocumentSnapshot` objects representing the results of a query. The documents

0 commit comments

Comments
 (0)