@react-native-firebase/firestore
Version:
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
414 lines (345 loc) • 10.9 kB
JavaScript
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { isNumber } from '@react-native-firebase/app/lib/common';
import FirestoreFieldPath, { DOCUMENT_ID } from './FirestoreFieldPath';
import { buildNativeArray, generateNativeData } from './utils/serialize';
export const OPERATORS = {
'==': 'EQUAL',
'>': 'GREATER_THAN',
'>=': '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',
};
const INEQUALITY = {
LESS_THAN: true,
LESS_THAN_OR_EQUAL: true,
GREATER_THAN: true,
GREATER_THAN_OR_EQUAL: true,
NOT_EQUAL: true,
};
const DIRECTIONS = {
asc: 'ASCENDING',
desc: 'DESCENDING',
};
export default class FirestoreQueryModifiers {
constructor() {
this._limit = undefined;
this._limitToLast = undefined;
this._filters = [];
this._orders = [];
this._type = 'collection';
// Cursors
this._startAt = undefined;
this._startAfter = undefined;
this._endAt = undefined;
this._endBefore = undefined;
// Pulled out of function to preserve their state
this.hasInequality = false;
this.hasNotEqual = false;
this.hasArrayContains = false;
this.hasArrayContainsAny = false;
this.hasIn = false;
this.hasNotIn = false;
}
_copy() {
const newInstance = new FirestoreQueryModifiers();
newInstance._limit = this._limit;
newInstance._limitToLast = this._limitToLast;
newInstance._filters = [...this._filters];
newInstance._orders = [...this._orders];
newInstance._type = this._type;
newInstance._startAt = this._startAt;
newInstance._startAfter = this._startAfter;
newInstance._endAt = this._endAt;
newInstance._endBefore = this._endBefore;
return newInstance;
}
get filters() {
return this._filters.map(f => ({
...f,
fieldPath: f.fieldPath instanceof FirestoreFieldPath ? f.fieldPath._toArray() : f.fieldPath,
}));
}
get orders() {
return this._orders.map(f => ({
...f,
fieldPath: f.fieldPath instanceof FirestoreFieldPath ? f.fieldPath._toArray() : f.fieldPath,
}));
}
get options() {
const options = {};
if (this._limit) {
options.limit = this._limit;
}
if (this._limitToLast) {
options.limitToLast = this._limitToLast;
}
if (this._startAt) {
options.startAt = this._startAt;
}
if (this._startAfter) {
options.startAfter = this._startAfter;
}
if (this._endAt) {
options.endAt = this._endAt;
}
if (this._endBefore) {
options.endBefore = this._endBefore;
}
return options;
}
get type() {
return this._type;
}
setFieldsCursor(cursor, fields) {
this[`_${cursor}`] = buildNativeArray(fields);
return this;
}
/**
* Options
*/
hasStart() {
return !!(this._startAt || this._startAfter);
}
hasEnd() {
return !!(this._endAt || this._endBefore);
}
/**
* Collection Group Query
*/
asCollectionGroupQuery() {
this._type = 'collectionGroup';
return this;
}
isCollectionGroupQuery() {
return this._type === 'collectionGroup';
}
/**
* Limit
*/
isValidLimit(limit) {
return !isNumber(limit) || Math.floor(limit) !== limit || limit <= 0;
}
limit(limit) {
this._limitToLast = undefined;
this._limit = limit;
return this;
}
/**
* limitToLast
*/
isValidLimitToLast(limit) {
return !isNumber(limit) || Math.floor(limit) !== limit || limit <= 0;
}
validatelimitToLast() {
if (this._limitToLast) {
if (!this._orders.length) {
throw new Error(
'firebase.firestore().collection().limitToLast() queries require specifying at least one firebase.firestore().collection().orderBy() clause',
);
}
}
}
limitToLast(limitToLast) {
this._limit = undefined;
this._limitToLast = limitToLast;
return this;
}
/**
* Filters
*/
isValidOperator(operator) {
return !!OPERATORS[operator];
}
isEqualOperator(operator) {
return OPERATORS[operator] === 'EQUAL';
}
isNotEqualOperator(operator) {
return OPERATORS[operator] === 'NOT_EQUAL';
}
isInOperator(operator) {
return (
OPERATORS[operator] === 'IN' ||
OPERATORS[operator] === 'ARRAY_CONTAINS_ANY' ||
OPERATORS[operator] === 'NOT_IN'
);
}
where(fieldPath, opStr, value) {
const filter = {
fieldPath,
operator: OPERATORS[opStr],
value: generateNativeData(value, true),
};
this._filters = this._filters.concat(filter);
return this;
}
filterWhere(filter) {
this._filters = this._filters.concat(filter);
return this;
}
validateWhere() {
if (this._filters.length > 0) {
this._filterCheck(this._filters);
}
}
_filterCheck(filters) {
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (filter.queries) {
// Recursively check sub-queries for Filters
this._filterCheck(filter.queries);
// If it is a Filter query, skip the rest of the loop
continue;
}
// Skip if no inequality
if (!INEQUALITY[filter.operator]) {
continue;
}
if (filter.operator === OPERATORS['!=']) {
if (this.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
this.hasNotEqual = true;
}
// Set the first inequality
if (!this.hasInequality) {
this.hasInequality = filter;
continue;
}
}
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (filter.operator === OPERATORS['array-contains']) {
if (this.hasArrayContains) {
throw new Error('Invalid query. Queries only support a single array-contains filter.');
}
this.hasArrayContains = true;
}
if (filter.operator === OPERATORS['array-contains-any']) {
if (this.hasArrayContainsAny) {
throw new Error(
"Invalid query. You cannot use more than one 'array-contains-any' filter.",
);
}
if (this.hasNotIn) {
throw new Error(
"Invalid query. You cannot use 'array-contains-any' filters with 'not-in' filters.",
);
}
this.hasArrayContainsAny = true;
}
if (filter.operator === OPERATORS.in) {
if (this.hasNotIn) {
throw new Error("Invalid query. You cannot use 'in' filters with 'not-in' filters.");
}
this.hasIn = true;
}
if (filter.operator === OPERATORS['not-in']) {
if (this.hasNotIn) {
throw new Error("Invalid query. You cannot use more than one 'not-in' filter.");
}
if (this.hasNotEqual) {
throw new Error(
"Invalid query. You cannot use 'not-in' filters with '!=' inequality filters",
);
}
if (this.hasIn) {
throw new Error("Invalid query. You cannot use 'not-in' filters with 'in' filters.");
}
if (this.hasArrayContainsAny) {
throw new Error(
"Invalid query. You cannot use 'not-in' filters with 'array-contains-any' filters.",
);
}
this.hasNotIn = true;
}
}
}
/**
* Orders
*/
isValidDirection(directionStr) {
return !!DIRECTIONS[directionStr.toLowerCase()];
}
orderBy(fieldPath, directionStr) {
const order = {
fieldPath: fieldPath,
direction: directionStr ? DIRECTIONS[directionStr.toLowerCase()] : DIRECTIONS.asc,
};
this._orders = this._orders.concat(order);
return this;
}
validateOrderBy() {
this._validateOrderByCheck(this._filters);
}
_validateOrderByCheck(filters) {
// Ensure order hasn't been called on the same field
if (this._orders.length > 1) {
const orders = this._orders.map($ => $.fieldPath._toPath());
const set = new Set(orders);
if (set.size !== orders.length) {
throw new Error('Invalid query. Order by clause cannot contain duplicate fields.');
}
}
// Skip if no where filters
if (filters.length === 0) {
return;
}
// Ensure the first order field path is equal to the inequality filter field path
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (filter.queries) {
// Recursively check sub-queries for Filters
this._validateOrderByCheck(filter.queries);
// If it is a Filter query, skip the rest of the loop
continue;
}
const filterFieldPath = filter.fieldPath._toPath();
for (let k = 0; k < this._orders.length; k++) {
const order = this._orders[k];
const orderFieldPath = order.fieldPath;
if (filter.operator === OPERATORS['==']) {
// Any where() fieldPath parameter cannot match any orderBy() parameter when '==' operand is invoked
if (filterFieldPath === orderFieldPath._toPath()) {
throw new Error(
`Invalid query. Query.orderBy() parameter: ${orderFieldPath} cannot be the same as your Query.where() fieldPath parameter: ${filterFieldPath}`,
);
}
}
if (filterFieldPath === DOCUMENT_ID._toPath() && orderFieldPath !== DOCUMENT_ID._toPath()) {
throw new Error(
"Invalid query. Query.where() fieldPath parameter: 'FirestoreFieldPath' cannot be used in conjunction with a different Query.orderBy() parameter",
);
}
if (INEQUALITY[filter.operator]) {
// Initial orderBy() parameter has to match every where() fieldPath parameter when inequality operator is invoked
if (filterFieldPath !== this._orders[0].fieldPath._toPath()) {
throw new Error(
`Invalid query. Initial Query.orderBy() parameter: ${orderFieldPath} has to be the same as the Query.where() fieldPath parameter(s): ${filterFieldPath} when an inequality operator is invoked `,
);
}
}
}
}
}
}