/* * 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 { isNull, isObject, isString, isUndefined } from '@react-native-firebase/app/lib/common'; import NativeError from '@react-native-firebase/app/lib/internal/NativeFirebaseError'; import FirestoreDocumentSnapshot from './FirestoreDocumentSnapshot'; import FirestoreFieldPath, { fromDotSeparatedString } from './FirestoreFieldPath'; import FirestoreQuerySnapshot from './FirestoreQuerySnapshot'; import { parseSnapshotArgs } from './utils'; let _id = 0; export default class FirestoreQuery { constructor(firestore, collectionPath, modifiers) { this._firestore = firestore; this._collectionPath = collectionPath; this._modifiers = modifiers; } get firestore() { return this._firestore; } _handleQueryCursor(cursor, docOrField, fields) { if (isUndefined(docOrField)) { throw new Error( `firebase.firestore().collection().${cursor}(*) Expected a DocumentSnapshot or list of field values but got undefined.`, ); } // Handles cases where the first arg is a DocumentSnapshot if (docOrField instanceof FirestoreDocumentSnapshot) { if (fields.length > 0) { throw new Error( `firebase.firestore().collection().${cursor}(*) Too many arguments provided. Expected DocumentSnapshot or list of field values.`, ); } const documentSnapshot = docOrField; if (!documentSnapshot.exists) { throw new Error( `firebase.firestore().collection().${cursor}(*) Can't use a DocumentSnapshot that doesn't exist.`, ); } const currentOrders = this._modifiers.orders; const values = []; for (let i = 0; i < currentOrders.length; i++) { const order = currentOrders[i]; const value = documentSnapshot.get(order.fieldPath); if (value === undefined) { throw new Error( `firebase.firestore().collection().${cursor}(*) You are trying to start or end a query using a document for which the field '${ order.fieldPath }' (used as the orderBy) does not exist.`, ); } values.push(value); } this._modifiers._orders.push({ fieldPath: '__name__', direction: 'ASCENDING', }); values.push(documentSnapshot.id); return this._modifiers.setFieldsCursor(cursor, values); } /** * Assumes list of field values to query by. Orders must be of equal length. */ const allFields = [docOrField].concat(fields); if (allFields.length > this._modifiers.orders.length) { throw new Error( `firebase.firestore().collection().${cursor}(*) Too many arguments provided. The number of arguments must be less than or equal to the number of orderBy() clauses.`, ); } return this._modifiers.setFieldsCursor(cursor, allFields); } endAt(docOrField, ...fields) { return new FirestoreQuery( this._firestore, this._collectionPath, this._handleQueryCursor('endAt', docOrField, fields), ); } endBefore(docOrField, ...fields) { return new FirestoreQuery( this._firestore, this._collectionPath, this._handleQueryCursor('endBefore', docOrField, fields), ); } get(options) { if (!isUndefined(options) && !isObject(options)) { throw new Error( "firebase.firestore().collection().get(*) 'options' must be an object is provided.", ); } if ( options && options.source && options.source !== 'default' && options.source !== 'server' && options.source !== 'cache' ) { throw new Error( "firebase.firestore().collection().get(*) 'options' GetOptions.source must be one of 'default', 'server' or 'cache'.", ); } return this._firestore.native .collectionGet( this._collectionPath.relativeName, this._modifiers.type, this._modifiers.filters, this._modifiers.orders, this._modifiers.options, options, ) .then(data => new FirestoreQuerySnapshot(this._firestore, this, data)); } isEqual(other) { if (!(other instanceof FirestoreQuery)) { throw new Error( "firebase.firestore().collection().isEqual(*) 'other' expected a Query instance.", ); } // Carry out lightweight checks first if ( this.firestore.app.name !== other.firestore.app.name || this._modifiers.type !== other._modifiers.type || this._modifiers.filters.length !== other._modifiers.filters.length || this._modifiers.orders.length !== other._modifiers.orders.length || Object.keys(this._modifiers.options).length !== Object.keys(other._modifiers.options).length ) { return false; } // Carry out potentially expensive checks // noinspection RedundantIfStatementJS if ( JSON.stringify(this._modifiers.filters) !== JSON.stringify(other._modifiers.filters) || JSON.stringify(this._modifiers.orders) !== JSON.stringify(other._modifiers.orders) || JSON.stringify(this._modifiers.options) !== JSON.stringify(other._modifiers.options) ) { return false; } return true; } limit(limit) { if (this._modifiers.isValidLimit(limit)) { throw new Error( "firebase.firestore().collection().limit(*) 'limit' must be a positive integer value.", ); } const modifiers = this._modifiers.limit(limit); return new FirestoreQuery(this._firestore, this._collectionPath, modifiers); } onSnapshot(...args) { let snapshotListenOptions; let callback; let onNext; let onError; try { const options = parseSnapshotArgs(args); snapshotListenOptions = options.snapshotListenOptions; callback = options.callback; onNext = options.onNext; onError = options.onError; } catch (e) { throw new Error(`firebase.firestore().collection().onSnapshot(*) ${e.message}`); } function handleSuccess(querySnapshot) { callback(querySnapshot, null); onNext(querySnapshot); } function handleError(error) { callback(null, error); onError(error); } const listenerId = _id++; const onSnapshotSubscription = this._firestore.emitter.addListener( this._firestore.eventNameForApp(`firestore_collection_sync_event:${listenerId}`), event => { if (event.body.error) { handleError(NativeError.fromEvent(event.body.error, 'firestore')); } else { const querySnapshot = new FirestoreQuerySnapshot( this._firestore, this, event.body.snapshot, ); handleSuccess(querySnapshot); } }, ); const unsubscribe = () => { onSnapshotSubscription.remove(); this._firestore.native.collectionOffSnapshot(listenerId); }; this._firestore.native.collectionOnSnapshot( this._collectionPath.relativeName, this._modifiers.type, this._modifiers.filters, this._modifiers.orders, this._modifiers.options, listenerId, snapshotListenOptions, ); return unsubscribe; } orderBy(fieldPath, directionStr) { if (!isString(fieldPath) && !(fieldPath instanceof FirestoreFieldPath)) { throw new Error( "firebase.firestore().collection().orderBy(*) 'fieldPath' must be a string or instance of FieldPath.", ); } let path; if (isString(fieldPath)) { try { path = fromDotSeparatedString(fieldPath); } catch (e) { throw new Error(`firebase.firestore().collection().orderBy(*) 'fieldPath' ${e.message}.`); } } else { path = fieldPath; } if (!isUndefined(directionStr) && !this._modifiers.isValidDirection(directionStr)) { throw new Error( "firebase.firestore().collection().orderBy(_, *) 'directionStr' must be one of 'asc' or 'desc'.", ); } if (this._modifiers.hasStart()) { throw new Error( 'firebase.firestore().collection().orderBy() Invalid query. You must not call startAt() or startAfter() before calling orderBy().', ); } if (this._modifiers.hasEnd()) { throw new Error( 'firebase.firestore().collection().orderBy() Invalid query. You must not call endAt() or endBefore() before calling orderBy().', ); } const modifiers = this._modifiers.orderBy(path, directionStr); try { modifiers.validateOrderBy(); } catch (e) { throw new Error(`firebase.firestore().collection().orderBy() ${e.message}`); } return new FirestoreQuery(this._firestore, this._collectionPath, modifiers); } startAfter(docOrField, ...fields) { return new FirestoreQuery( this._firestore, this._collectionPath, this._handleQueryCursor('startAfter', docOrField, fields), ); } startAt(docOrField, ...fields) { return new FirestoreQuery( this._firestore, this._collectionPath, this._handleQueryCursor('startAt', docOrField, fields), ); } where(fieldPath, opStr, value) { if (!isString(fieldPath) && !(fieldPath instanceof FirestoreFieldPath)) { throw new Error( "firebase.firestore().collection().where(*) 'fieldPath' must be a string or instance of FieldPath.", ); } let path; if (isString(fieldPath)) { try { path = fromDotSeparatedString(fieldPath); } catch (e) { throw new Error(`firebase.firestore().collection().where(*) 'fieldPath' ${e.message}.`); } } else { path = fieldPath; } if (!this._modifiers.isValidOperator(opStr)) { throw new Error( "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=' or 'array-contains'.", ); } if (isUndefined(value)) { throw new Error( "firebase.firestore().collection().where(_, _, *) 'value' argument expected.", ); } if (isNull(value) && !this._modifiers.isEqualOperator(opStr)) { throw new Error( "firebase.firestore().collection().where(_, _, *) 'value' is invalid. You can only perform equals comparisons on null", ); } const modifiers = this._modifiers.where(path, opStr, value); try { modifiers.validateWhere(); } catch (e) { throw new Error(`firebase.firestore().collection().where() ${e.message}`); } modifiers.validateWhere(); return new FirestoreQuery(this._firestore, this._collectionPath, modifiers); } }