/* * 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 { isArray, isNull, isObject, isString, isUndefined, } from '@react-native-firebase/app/dist/module/common'; import NativeError from '@react-native-firebase/app/dist/module/internal/NativeFirebaseError'; import { AggregateQuery } from './FirestoreAggregate'; import DocumentSnapshot from './FirestoreDocumentSnapshot'; import { FieldPath, fromDotSeparatedString } from './FieldPath'; import { _Filter, generateFilters } from './FirestoreFilter'; import QueryModifiers, { getInOperatorValueLimit } from './FirestoreQueryModifiers'; import QuerySnapshot, { type QuerySnapshotNativeData } from './FirestoreQuerySnapshot'; import { parseSnapshotArgs, throwIfLiteSdkSnapshotListenerUnsupported, validateWithConverter, } from './utils'; import type FirestorePath from './FirestorePath'; import type { DocumentData, FirestoreDataConverter, ListenSource, Query as QueryInterface, } from './types/firestore'; import type { FirestoreInternal, DocumentFieldValueInternal, FirestoreSyncEventErrorInternal, FirestoreSyncEventBodyInternal, } from './types/internal'; let _id = 0; /** @internal */ export class Query< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, > implements QueryInterface { _firestore: FirestoreInternal; _collectionPath: FirestorePath; _modifiers: QueryModifiers; _queryName: string | undefined; _converter: FirestoreDataConverter | null; /** Satisfies Query for AggregateQuerySnapshot and similar. */ readonly type: 'query' | 'collection' = 'query'; constructor( firestore: FirestoreInternal, collectionPath: FirestorePath, modifiers: QueryModifiers, queryName?: string, converter?: FirestoreDataConverter | null, ) { this._firestore = firestore; this._collectionPath = collectionPath; this._modifiers = modifiers; this._queryName = queryName; this._converter = converter ?? null; } get firestore(): FirestoreInternal { return this._firestore; } get converter(): FirestoreDataConverter | null { return this._converter; } _handleQueryCursor( cursor: 'startAt' | 'startAfter' | 'endAt' | 'endBefore', docOrField: DocumentSnapshot | DocumentFieldValueInternal, fields: DocumentFieldValueInternal[], ): QueryModifiers { const modifiers = this._modifiers._copy(); if (isUndefined(docOrField)) { throw new Error( `firebase.firestore().collection().${cursor}(*) Expected a DocumentSnapshot or list of field values but got undefined.`, ); } if (docOrField instanceof DocumentSnapshot) { 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 = modifiers.orders; const values: DocumentFieldValueInternal[] = []; for (let i = 0; i < currentOrders.length; i++) { const order = currentOrders[i]!; const pathStr = Array.isArray(order.fieldPath) ? order.fieldPath.join('.') : String(order.fieldPath); if (pathStr === '__name__') { continue; } const value = documentSnapshot.get( Array.isArray(order.fieldPath) ? new FieldPath(...order.fieldPath) : (order.fieldPath as string | 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 '${pathStr}' (used as the orderBy) does not exist.`, ); } values.push(value); } if (modifiers._orders.length) { const lastOrder = modifiers._orders[modifiers._orders.length - 1]!; const lastPathStr = lastOrder.fieldPath instanceof FieldPath ? lastOrder.fieldPath._toPath() : String(lastOrder.fieldPath); if (lastPathStr !== '__name__') { modifiers._orders.push({ fieldPath: new FieldPath('__name__'), direction: lastOrder.direction, }); } } else { modifiers._orders.push({ fieldPath: new FieldPath('__name__'), direction: 'ASCENDING', }); } if (this._modifiers.isCollectionGroupQuery()) { values.push(documentSnapshot.ref.path); } else { values.push(documentSnapshot.id); } return modifiers.setFieldsCursor(cursor, values); } const allFields = [docOrField].concat(fields); if (allFields.length > 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 modifiers.setFieldsCursor(cursor, allFields); } count(): AggregateQuery { return new AggregateQuery( this._firestore, this as unknown as Query, this._collectionPath, this._modifiers, ); } countFromServer(): ReturnType { return this.count(); } endAt( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] ): Query { return new Query( this._firestore, this._collectionPath, this._handleQueryCursor( 'endAt', docOrField as DocumentSnapshot | DocumentFieldValueInternal, fields, ), this._queryName, this._converter, ); } endBefore( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] ): Query { return new Query( this._firestore, this._collectionPath, this._handleQueryCursor( 'endBefore', docOrField as DocumentSnapshot | DocumentFieldValueInternal, fields, ), this._queryName, this._converter, ); } get(options?: { source?: 'default' | 'server' | 'cache'; }): Promise> { 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'.", ); } if (!isUndefined(this._queryName)) { return this._firestore.native .namedQueryGet( this._queryName, this._modifiers.type, this._modifiers.filters, this._modifiers.orders, this._modifiers.options, options, ) .then( (data: unknown) => new QuerySnapshot( this._firestore, this, data as QuerySnapshotNativeData, this._converter, ), ); } this._modifiers.validatelimitToLast(); return this._firestore.native .collectionGet( this._collectionPath.relativeName, this._modifiers.type, this._modifiers.filters, this._modifiers.orders, this._modifiers.options, options, ) .then( (data: unknown) => new QuerySnapshot( this._firestore, this, data as QuerySnapshotNativeData, this._converter, ), ); } isEqual(other: Query): boolean { if (!(other instanceof Query)) { throw new Error( "firebase.firestore().collection().isEqual(*) 'other' expected a Query instance.", ); } 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 || this._collectionPath.relativeName !== other._collectionPath.relativeName || this._converter !== other._converter || Object.keys(this._modifiers.options).length !== Object.keys(other._modifiers.options).length ) { return false; } 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: number): Query { if (this._modifiers.isValidLimit(limit)) { throw new Error( "firebase.firestore().collection().limit(*) 'limit' must be a positive integer value.", ); } const modifiers = this._modifiers._copy().limit(limit); return new Query( this._firestore, this._collectionPath, modifiers, this._queryName, this._converter, ); } limitToLast(limitToLast: number): Query { if (this._modifiers.isValidLimitToLast(limitToLast)) { throw new Error( "firebase.firestore().collection().limitToLast(*) 'limitToLast' must be a positive integer value.", ); } const modifiers = this._modifiers._copy().limitToLast(limitToLast); return new Query( this._firestore, this._collectionPath, modifiers, this._queryName, this._converter, ); } onSnapshot(...args: unknown[]): () => void { throwIfLiteSdkSnapshotListenerUnsupported(); let snapshotListenOptions: { includeMetadataChanges?: boolean; source?: ListenSource }; let callback: ( snapshot: QuerySnapshot | null, error: Error | null, ) => void; let onNext: (snapshot: QuerySnapshot) => void; let onError: (error: Error) => void; this._modifiers.validatelimitToLast(); 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 as Error).message}`); } function handleSuccess(querySnapshot: QuerySnapshot): void { callback(querySnapshot, null); onNext(querySnapshot); } function handleError(error: Error): void { callback(null, error); onError(error); } const listenerId = _id++; const onSnapshotSubscription = this._firestore.emitter.addListener( this._firestore.eventNameForApp(`firestore_collection_sync_event:${listenerId}`), (event: { body: FirestoreSyncEventBodyInternal }) => { const body = event.body as { error?: FirestoreSyncEventErrorInternal; snapshot?: QuerySnapshotNativeData; }; if (body.error) { handleError(NativeError.fromEvent(body.error, 'firestore')); } else { const snapshot = body.snapshot; if (!snapshot) return; const querySnapshot = new QuerySnapshot(this._firestore, this, snapshot, this._converter); handleSuccess(querySnapshot); } }, ); const unsubscribe = (): void => { onSnapshotSubscription.remove(); this._firestore.native.collectionOffSnapshot(listenerId); }; if (!isUndefined(this._queryName)) { this._firestore.native.namedQueryOnSnapshot( this._queryName, this._modifiers.type, this._modifiers.filters, this._modifiers.orders, this._modifiers.options, listenerId, snapshotListenOptions, ); } else { 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: string | FieldPath, directionStr?: string): Query { if (!isString(fieldPath) && !(fieldPath instanceof FieldPath)) { throw new Error( "firebase.firestore().collection().orderBy(*) 'fieldPath' must be a string or instance of FieldPath.", ); } let path: FieldPath; if (isString(fieldPath)) { try { path = fromDotSeparatedString(fieldPath); } catch (e) { throw new Error( `firebase.firestore().collection().orderBy(*) 'fieldPath' ${(e as Error).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._copy().orderBy(path, directionStr); try { modifiers.validateOrderBy(); } catch (e) { throw new Error(`firebase.firestore().collection().orderBy() ${(e as Error).message}`); } return new Query( this._firestore, this._collectionPath, modifiers, this._queryName, this._converter, ); } startAfter( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] ): Query { return new Query( this._firestore, this._collectionPath, this._handleQueryCursor('startAfter', docOrField, fields), this._queryName, this._converter, ); } startAt( docOrField: DocumentSnapshot | DocumentFieldValueInternal, ...fields: DocumentFieldValueInternal[] ): Query { return new Query( this._firestore, this._collectionPath, this._handleQueryCursor('startAt', docOrField, fields), this._queryName, this._converter, ); } where( fieldPathOrFilter: string | FieldPath | _Filter, opStr?: string, value?: DocumentFieldValueInternal, ): Query { if ( !isString(fieldPathOrFilter) && !(fieldPathOrFilter instanceof FieldPath) && !(fieldPathOrFilter instanceof _Filter) ) { throw new Error( "firebase.firestore().collection().where(*) 'fieldPath' must be a string, instance of FieldPath or instance of Filter.", ); } let modifiers: QueryModifiers; if (fieldPathOrFilter instanceof _Filter && fieldPathOrFilter.queries) { const filters = generateFilters(fieldPathOrFilter, this._modifiers); modifiers = this._modifiers._copy().filterWhere(filters); } else { let path: FieldPath; let op = opStr; let val = value; if (fieldPathOrFilter instanceof _Filter) { op = fieldPathOrFilter.operator as string; val = fieldPathOrFilter.value; fieldPathOrFilter = fieldPathOrFilter.fieldPath as FieldPath; } if (isString(fieldPathOrFilter)) { try { path = fromDotSeparatedString(fieldPathOrFilter); } catch (e) { throw new Error( `firebase.firestore().collection().where(*) 'fieldPath' ${(e as Error).message}.`, ); } } else { path = fieldPathOrFilter; } if (!this._modifiers.isValidOperator(op!)) { throw new Error( "firebase.firestore().collection().where(_, *) 'opStr' is invalid. Expected one of '==', '>', '>=', '<', '<=', '!=', 'array-contains', 'not-in', 'array-contains-any' or 'in'.", ); } if (isUndefined(val)) { throw new Error( "firebase.firestore().collection().where(_, _, *) 'value' argument expected.", ); } if ( isNull(val) && !this._modifiers.isEqualOperator(op!) && !this._modifiers.isNotEqualOperator(op!) ) { throw new Error( "firebase.firestore().collection().where(_, _, *) 'value' is invalid. You can only perform equals comparisons on null", ); } if (this._modifiers.isInOperator(op!)) { if (!isArray(val) || !(val as DocumentFieldValueInternal[]).length) { throw new Error( `firebase.firestore().collection().where(_, _, *) 'value' is invalid. A non-empty array is required for '${op}' filters.`, ); } const maxValues = getInOperatorValueLimit(op!); if ((val as DocumentFieldValueInternal[]).length > maxValues) { throw new Error( `firebase.firestore().collection().where(_, _, *) 'value' is invalid. '${op}' filters support a maximum of ${maxValues} elements in the value array.`, ); } } modifiers = this._modifiers._copy().where(path, op!, val); } try { modifiers.validateWhere(); } catch (e) { throw new Error(`firebase.firestore().collection().where() ${(e as Error).message}`); } return new Query( this._firestore, this._collectionPath, modifiers, this._queryName, this._converter, ); } withConverter(converter: null): Query; withConverter( converter: FirestoreDataConverter, ): Query; withConverter( converter: FirestoreDataConverter | null | unknown, ): Query | Query { if (isUndefined(converter) || isNull(converter)) { return new Query( this._firestore, this._collectionPath, this._modifiers, this._queryName, null, ); } try { validateWithConverter(converter); } catch (e) { throw new Error(`firebase.firestore().collection().withConverter() ${(e as Error).message}`); } return new Query( this._firestore, this._collectionPath, this._modifiers, this._queryName, converter as FirestoreDataConverter, ); } } export default Query;