Skip to content

Commit

Permalink
feat(firestore): query operators: 'not-in' & '!=' (#4474)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
russellwheatley authored Oct 30, 2020
1 parent ee9edff commit 9e68faf
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -95,6 +98,9 @@ private void applyFilters(ReadableArray filters) {
case "IN":
query = query.whereIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
break;
case "NOT_IN":
query = query.whereNotIn(Objects.requireNonNull(fieldPath), Objects.requireNonNull((List<Object>) value));
break;
}
}
}
Expand Down
133 changes: 133 additions & 0 deletions packages/firestore/e2e/Query/where.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
4 changes: 4 additions & 0 deletions packages/firestore/ios/RNFBFirestore/RNFBFirestoreQuery.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"]) {
Expand All @@ -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];
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/firestore/lib/FirestoreQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'.",
);
}

Expand Down
55 changes: 53 additions & 2 deletions packages/firestore/lib/FirestoreQueryModifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand All @@ -35,6 +37,7 @@ const INEQUALITY = {
LESS_THAN_OR_EQUAL: true,
GREATER_THAN: true,
GREATER_THAN_OR_EQUAL: true,
NOT_EQUAL: true,
};

const DIRECTIONS = {
Expand Down Expand Up @@ -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) {
Expand All @@ -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];
Expand All @@ -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;
Expand All @@ -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()}'`,
);
}
}
Expand All @@ -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];
Expand All @@ -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;
}

Expand All @@ -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;
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/firestore/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>;
Expand All @@ -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
Expand Down
Loading

1 comment on commit 9e68faf

@vercel
Copy link

@vercel vercel bot commented on 9e68faf Oct 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.