diff --git a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java index b2d85ecc9b..b16a8ca3ed 100644 --- a/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java +++ b/packages/firestore/android/src/reactnative/java/io/invertase/firebase/firestore/ReactNativeFirebaseFirestoreQuery.java @@ -74,6 +74,9 @@ private void applyFilters(ReadableArray filters) { case "EQUAL": query = query.whereEqualTo(Objects.requireNonNull(fieldPath), value); break; + case "NOT_EQUAL": + query = query.whereNotEqualTo(Objects.requireNonNull(fieldPath), value); + break; case "GREATER_THAN": query = query.whereGreaterThan(Objects.requireNonNull(fieldPath), Objects.requireNonNull(value)); break; @@ -95,6 +98,9 @@ private void applyFilters(ReadableArray filters) { case "IN": query = query.whereIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List) value)); break; + case "NOT_IN": + query = query.whereNotIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List) value)); + break; } } } diff --git a/packages/firestore/e2e/Query/where.e2e.js b/packages/firestore/e2e/Query/where.e2e.js index 598e78a4e1..35d6ce65cc 100644 --- a/packages/firestore/e2e/Query/where.e2e.js +++ b/packages/firestore/e2e/Query/where.e2e.js @@ -436,4 +436,137 @@ describe('firestore().collection().where()', () => { items.length.should.equal(1); }); + + it("should correctly retrieve data when using 'not-in' operator", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + await Promise.all([ref.add({ notIn: 'here' }), ref.add({ notIn: 'now' })]); + + const result = await ref.where('notIn', 'not-in', ['here', 'there', 'everywhere']).get(); + should(result.docs.length).equal(1); + should(result.docs[0].data().notIn).equal('now'); + }); + + it("should throw error when using 'not-in' operator twice", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'not-in', [1]).where('test', 'not-in', [2]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one 'not-in' filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with '!=' operator", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with '!=' inequality filters", + ); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'in' operator", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'in', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use 'not-in' filters with 'in' filters."); + return Promise.resolve(); + } + }); + + it("should throw error when combining 'not-in' operator with 'array-contains-any' operator", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'array-contains-any', [2]).where('test', 'not-in', [1]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + "You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + return Promise.resolve(); + } + }); + + it("should throw error when 'not-in' filter has a list of more than 10 items", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', 'not-in', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql( + 'filters support a maximum of 10 elements in the value array.', + ); + return Promise.resolve(); + } + }); + + it("should correctly retrieve data when using '!=' operator", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + await Promise.all([ref.add({ notEqual: 'here' }), ref.add({ notEqual: 'now' })]); + + const result = await ref.where('notEqual', '!=', 'here').get(); + + should(result.docs.length).equal(1); + should(result.docs[0].data().notEqual).equal('now'); + }); + + it("should throw error when using '!=' operator twice ", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('test', '!=', 2); + return Promise.reject(new Error('Did not throw an Error.')); + } catch (error) { + error.message.should.containEql("You cannot use more than one '!=' inequality filter."); + return Promise.resolve(); + } + }); + + it("should throw error when combining '!=' operator with any other inequality operator on a different field", async () => { + const ref = firebase.firestore().collection(COLLECTION); + + try { + ref.where('test', '!=', 1).where('differentField', '>', 1); + return Promise.reject(new Error('Did not throw an Error on >.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '<', 1); + return Promise.reject(new Error('Did not throw an Error on <.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '<=', 1); + return Promise.reject(new Error('Did not throw an Error <=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + try { + ref.where('test', '!=', 1).where('differentField', '>=', 1); + return Promise.reject(new Error('Did not throw an Error >=.')); + } catch (error) { + error.message.should.containEql('must be on the same field.'); + } + + return Promise.resolve(); + }); }); diff --git a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.m b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.m index 2d604fa8db..c2ff435c85 100644 --- a/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.m +++ b/packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.m @@ -58,6 +58,8 @@ - (void)applyFilters { if ([operator isEqualToString:@"EQUAL"]) { _query = [_query queryWhereFieldPath:fieldPath isEqualTo:value]; + } else if ([operator isEqualToString:@"NOT_EQUAL"]) { + _query = [_query queryWhereFieldPath:fieldPath isNotEqualTo:value]; } else if ([operator isEqualToString:@"GREATER_THAN"]) { _query = [_query queryWhereFieldPath:fieldPath isGreaterThan:value]; } else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) { @@ -72,6 +74,8 @@ - (void)applyFilters { _query = [_query queryWhereFieldPath:fieldPath in:value]; } else if ([operator isEqualToString:@"ARRAY_CONTAINS_ANY"]) { _query = [_query queryWhereFieldPath:fieldPath arrayContainsAny:value]; + } else if ([operator isEqualToString:@"NOT_IN"]) { + _query = [_query queryWhereFieldPath:fieldPath notIn:value]; } } } diff --git a/packages/firestore/lib/FirestoreQuery.js b/packages/firestore/lib/FirestoreQuery.js index 444ff54f51..8226ec13dd 100644 --- a/packages/firestore/lib/FirestoreQuery.js +++ b/packages/firestore/lib/FirestoreQuery.js @@ -383,7 +383,7 @@ export default class FirestoreQuery { if (!this._modifiers.isValidOperator(opStr)) { throw new Error( - "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', 'array-contains', 'array-contains-any' or 'in'.", + "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.", ); } diff --git a/packages/firestore/lib/FirestoreQueryModifiers.js b/packages/firestore/lib/FirestoreQueryModifiers.js index 2bc17b78b8..d9facea881 100644 --- a/packages/firestore/lib/FirestoreQueryModifiers.js +++ b/packages/firestore/lib/FirestoreQueryModifiers.js @@ -25,8 +25,10 @@ const OPERATORS = { '>=': 'GREATER_THAN_OR_EQUAL', '<': 'LESS_THAN', '<=': 'LESS_THAN_OR_EQUAL', + '!=': 'NOT_EQUAL', 'array-contains': 'ARRAY_CONTAINS', 'array-contains-any': 'ARRAY_CONTAINS_ANY', + 'not-in': 'NOT_IN', in: 'IN', }; @@ -35,6 +37,7 @@ const INEQUALITY = { LESS_THAN_OR_EQUAL: true, GREATER_THAN: true, GREATER_THAN_OR_EQUAL: true, + NOT_EQUAL: true, }; const DIRECTIONS = { @@ -190,7 +193,11 @@ export default class FirestoreQueryModifiers { } isInOperator(operator) { - return OPERATORS[operator] === 'IN' || OPERATORS[operator] === 'ARRAY_CONTAINS_ANY'; + return ( + OPERATORS[operator] === 'IN' || + OPERATORS[operator] === 'ARRAY_CONTAINS_ANY' || + OPERATORS[operator] === 'NOT_IN' + ); } where(fieldPath, opStr, value) { @@ -206,6 +213,7 @@ export default class FirestoreQueryModifiers { validateWhere() { let hasInequality; + let hasNotEqual; for (let i = 0; i < this._filters.length; i++) { const filter = this._filters[i]; @@ -214,6 +222,14 @@ export default class FirestoreQueryModifiers { continue; } + if (filter.operator === OPERATORS['!=']) { + if (hasNotEqual) { + throw new Error("Invalid query. You cannot use more than one '!=' inequality filter."); + } + //needs to set hasNotEqual = true before setting first hasInequality = filter. It is used in a condition check later + hasNotEqual = true; + } + // Set the first inequality if (!hasInequality) { hasInequality = filter; @@ -224,7 +240,7 @@ export default class FirestoreQueryModifiers { if (INEQUALITY[filter.operator] && hasInequality) { if (hasInequality.fieldPath._toPath() !== filter.fieldPath._toPath()) { throw new Error( - `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()}'`, + `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()}'`, ); } } @@ -233,6 +249,7 @@ export default class FirestoreQueryModifiers { let hasArrayContains; let hasArrayContainsAny; let hasIn; + let hasNotIn; for (let i = 0; i < this._filters.length; i++) { const filter = this._filters[i]; @@ -257,6 +274,12 @@ export default class FirestoreQueryModifiers { ); } + if (hasNotIn) { + throw new Error( + "Invalid query. You cannot use 'array-contains-any' filters with 'not-in' filters.", + ); + } + hasArrayContainsAny = true; } @@ -271,8 +294,36 @@ export default class FirestoreQueryModifiers { ); } + if (hasNotIn) { + throw new Error("Invalid query. You cannot use 'in' filters with 'not-in' filters."); + } + hasIn = true; } + + if (filter.operator === OPERATORS['not-in']) { + if (hasNotIn) { + throw new Error("Invalid query. You cannot use more than one 'not-in' filter."); + } + + if (hasNotEqual) { + throw new Error( + "Invalid query. You cannot use 'not-in' filters with '!=' inequality filters", + ); + } + + if (hasIn) { + throw new Error("Invalid query. You cannot use 'not-in' filters with 'in' filters."); + } + + if (hasArrayContainsAny) { + throw new Error( + "Invalid query. You cannot use 'not-in' filters with 'array-contains-any' filters.", + ); + } + + hasNotIn = true; + } } } diff --git a/packages/firestore/lib/index.d.ts b/packages/firestore/lib/index.d.ts index 0c17e0dbef..6fbb4eaa86 100644 --- a/packages/firestore/lib/index.d.ts +++ b/packages/firestore/lib/index.d.ts @@ -1240,7 +1240,7 @@ export namespace FirebaseFirestoreTypes { * ``` * * @param fieldPath The path to compare. - * @param opStr The operation string (e.g "<", "<=", "==", ">", ">=", "array-contains", "array-contains-any", "in"). + * @param opStr The operation string (e.g "<", "<=", "==", ">", ">=", "!=", "array-contains", "array-contains-any", "in", "not-in"). * @param value The comparison value. */ where(fieldPath: keyof T | FieldPath, opStr: WhereFilterOp, value: any): Query; @@ -1255,9 +1255,11 @@ export namespace FirebaseFirestoreTypes { | '==' | '>' | '>=' + | '!=' | 'array-contains' | 'array-contains-any' - | 'in'; + | 'in' + | 'not-in'; /** * A `QuerySnapshot` contains zero or more `QueryDocumentSnapshot` objects representing the results of a query. The documents diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index de97a59acf..6ed96bba33 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -757,64 +757,64 @@ PODS: - React-cxxreact (= 0.62.2) - React-jsi (= 0.62.2) - ReactCommon/callinvoker (= 0.62.2) - - RNFBAdMob (7.6.8): + - RNFBAdMob (7.6.9): - Firebase/AdMob (= 6.34.0) - PersonalizedAdConsent (~> 1.0.4) - React-Core - RNFBApp - - RNFBAnalytics (7.6.7): + - RNFBAnalytics (7.6.8): - Firebase/Analytics (= 6.34.0) - React-Core - RNFBApp - - RNFBApp (8.4.5): + - RNFBApp (8.4.6): - Firebase/CoreOnly (= 6.34.0) - React-Core - - RNFBAuth (9.3.0): + - RNFBAuth (9.3.1): - Firebase/Auth (= 6.34.0) - React-Core - RNFBApp - - RNFBCrashlytics (8.4.9): + - RNFBCrashlytics (8.4.11): - Firebase/Crashlytics (= 6.34.0) - React-Core - RNFBApp - - RNFBDatabase (7.5.11): + - RNFBDatabase (7.5.12): - Firebase/Database (= 6.34.0) - React-Core - RNFBApp - - RNFBDynamicLinks (7.5.9): + - RNFBDynamicLinks (7.5.10): - Firebase/DynamicLinks (= 6.34.0) - GoogleUtilities/AppDelegateSwizzler - React-Core - RNFBApp - - RNFBFirestore (7.8.6): + - RNFBFirestore (7.8.7): - Firebase/Firestore (= 6.34.0) - React-Core - RNFBApp - - RNFBFunctions (7.4.8): + - RNFBFunctions (7.4.9): - Firebase/Functions (= 6.34.0) - React-Core - RNFBApp - - RNFBIid (7.4.8): + - RNFBIid (7.4.9): - Firebase/CoreOnly (= 6.34.0) - FirebaseInstanceID - React-Core - RNFBApp - - RNFBInAppMessaging (7.5.6): + - RNFBInAppMessaging (7.5.7): - Firebase/InAppMessaging (= 6.34.0) - React-Core - RNFBApp - - RNFBMessaging (7.9.0): + - RNFBMessaging (7.9.1): - Firebase/Messaging (= 6.34.0) - React-Core - RNFBApp - - RNFBMLNaturalLanguage (7.4.8): + - RNFBMLNaturalLanguage (7.4.9): - Firebase/MLCommon (= 6.34.0) - Firebase/MLNaturalLanguage (= 6.34.0) - Firebase/MLNLLanguageID (= 6.34.0) - Firebase/MLNLSmartReply (= 6.34.0) - React-Core - RNFBApp - - RNFBMLVision (7.4.8): + - RNFBMLVision (7.4.11): - Firebase/MLVision (= 6.34.0) - Firebase/MLVisionBarcodeModel (= 6.34.0) - Firebase/MLVisionFaceModel (= 6.34.0) @@ -822,15 +822,15 @@ PODS: - Firebase/MLVisionTextModel (= 6.34.0) - React-Core - RNFBApp - - RNFBPerf (7.4.8): + - RNFBPerf (7.4.9): - Firebase/Performance (= 6.34.0) - React-Core - RNFBApp - - RNFBRemoteConfig (9.0.10): + - RNFBRemoteConfig (9.0.11): - Firebase/RemoteConfig (= 6.34.0) - React-Core - RNFBApp - - RNFBStorage (7.4.9): + - RNFBStorage (7.4.10): - Firebase/Storage (= 6.34.0) - React-Core - RNFBApp @@ -1091,25 +1091,25 @@ SPEC CHECKSUMS: React-RCTText: fae545b10cfdb3d247c36c56f61a94cfd6dba41d React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 - RNFBAdMob: 8008db9b4fb348ee1bcd707f066fa760873ea8da - RNFBAnalytics: 354446ab54bd1e094d82c33d1b232f3c70a4a90e - RNFBApp: c62b6f3442edf56ed004bd77240347647475ec9e - RNFBAuth: 6e8d1412db2ee67db4c8514ce95ee458195c6e70 - RNFBCrashlytics: 7da01fba0dd592b3c2d9ee1b088bd747df36d458 - RNFBDatabase: c8d58137acc264075ac1f637f9944e6d97bbf1af - RNFBDynamicLinks: 2227e2731a457e51c2ceca4dbccda1148a6a63c7 - RNFBFirestore: 455c682df9e0e323177c42383f6205ae39533b15 - RNFBFunctions: 4ca7ed9a30f5791f418f714346c301c31b8cb913 - RNFBIid: 0bb305b1accc30ed81b94acb4d1fbb1cdb9d97af - RNFBInAppMessaging: 13d0d207fdd8c24a07857b2c8befdc4848c8a7af - RNFBMessaging: bb1343d10652c5dd9c18124fd2f0246881203274 - RNFBMLNaturalLanguage: 919804054a99e98817fe6cd63904a58f26d45111 - RNFBMLVision: ff4bbc32b67f80064f1510eb0275ffb3ac829f88 - RNFBPerf: f95655c914cc4b1c3c829b68465fbe59dfb5e7b3 - RNFBRemoteConfig: 134e3fdc55d6d5aee69d9c083c13f8694b6510c3 - RNFBStorage: 442afb7abb614ee44ecbdd8a7bec42e6d4210302 + RNFBAdMob: 809f648889201406d333bc28a84bbf3294491f00 + RNFBAnalytics: 159651d6eae3c85db38ba5d694d8c6c46fd3883c + RNFBApp: e0fc0113eecc07f440f17639c9b7c59ea90bc583 + RNFBAuth: 16207757fa69ad50ec8ca04964f59cd560979294 + RNFBCrashlytics: c85d01c3fb3a4cc1e762facb9d4ad26b95f7f9dc + RNFBDatabase: 6c01157824702f4fc1cedf9b4b95e9f3154cfbf1 + RNFBDynamicLinks: 067d7419d8daf58b61faa70834b051410d5f6d4b + RNFBFirestore: 64986e129f4980e73b6e510684286d986367bef6 + RNFBFunctions: ae7a279090e902cdf4da7890d64f31a0e4e2a825 + RNFBIid: f40ac75229f8bb86cc6dd0c71b450e7df693f8f3 + RNFBInAppMessaging: dda5e571edd579042fa1d68a685012daa871a3f6 + RNFBMessaging: 1d2a6a249cf6b93bed1721befc42650c45615112 + RNFBMLNaturalLanguage: 3662457b2e95b857bb9289d250b0a10bc10aca84 + RNFBMLVision: c2547c24b59298ebe4b90a2025600d60a6929930 + RNFBPerf: 0c08e45726f7a19487b79cef3d12ee7e917c8b7a + RNFBRemoteConfig: 85df9235f46c20e293257b6e481412ac585e2966 + RNFBStorage: 72404d4977261c0d7060e87c3d0cf7f7e060c6a3 Yoga: 3ebccbdd559724312790e7742142d062476b698e -PODFILE CHECKSUM: 555dacf747a9f802fb0d5efa4c6f558ca6129c4d +PODFILE CHECKSUM: 2b670e97319c5057f900292e8500c4c38d83aa3c -COCOAPODS: 1.10.0.rc.1 +COCOAPODS: 1.10.0