/* * 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 { _Filter, Filter } from '../FirestoreFilter'; import type { DocumentData, DocumentReference, DocumentSnapshot, OrderByDirection, Query, QueryConstraintType, QuerySnapshot, WhereFilterOp, } from '../types/firestore'; import type { DocumentReferenceDeleteInternal, DocumentReferenceGetInternal, QueryInternal, QueryWithMethodInternal, QueryWithWhereInternal, ReferenceIsEqualInternal, } from '../types/internal'; import type { FieldPath } from '../FieldPath'; function isCompositeFilterConstraint( value: QueryFilterConstraint, ): value is QueryCompositeFilterConstraint { return value instanceof QueryCompositeFilterConstraint; } function getFilterFromConstraint(constraint: QueryFilterConstraint): _Filter { if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } return (constraint as QueryFieldFilterConstraint)._filter; } /** * Abstraction of a constraint that can be applied to a Firestore query. * Not exported — public API matches firebase-js-sdk ({@link QueryConstraint} only). */ abstract class AppliableConstraint { abstract _apply( query: Query, ): Query; } /** * A `QueryConstraint` is used to narrow the set of documents returned by a * Firestore query. `QueryConstraint`s are created by invoking {@link where}, * {@link orderBy}, {@link startAt}, {@link startAfter}, {@link endBefore}, * {@link endAt}, {@link limit}, {@link limitToLast} and can then be passed to * {@link query} to create a new query instance that also contains this `QueryConstraint`. */ export abstract class QueryConstraint extends AppliableConstraint { abstract readonly type: QueryConstraintType; _apply( _queryRef: Query, ): Query { throw new Error('_apply must be implemented by subclass'); } } /** * Shared _apply for orderBy/limit/startAt/endAt/where. Not exported — implementation detail. */ abstract class QueryConstraintBase extends QueryConstraint { abstract readonly type: QueryConstraintType; private readonly _args: unknown[]; protected constructor(...args: unknown[]) { super(); this._args = args; } _apply( query: Query, ): Query { const method = (query as unknown as QueryWithMethodInternal)[ this.type ]; if (!method) { throw new Error(`Query method '${this.type}' is not available on query instance.`); } return method.call(query, ...this._args); } } export class QueryCompositeFilterConstraint extends AppliableConstraint { /** * @internal */ protected constructor( /** The type of this query constraint */ readonly type: 'or' | 'and', private readonly _queryConstraints: QueryFilterConstraint[], ) { super(); } static _create( type: 'or' | 'and', _queryConstraints: QueryFilterConstraint[], ): QueryCompositeFilterConstraint { // Validate nested OR filters when creating the constraint if (type === 'or') { const filters = _queryConstraints.map(constraint => { if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } return getFilterFromConstraint(constraint); }); // This will throw if nested OR filters are detected Filter.or(...filters); } return new QueryCompositeFilterConstraint(type, _queryConstraints); } _apply( query: Query, ): Query { const filters = this._queryConstraints.map(constraint => { if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } return getFilterFromConstraint(constraint); }); const _filter = this.type === 'or' ? Filter.or(...filters) : Filter.and(...filters); const where = (query as unknown as QueryWithWhereInternal).where; return where.call(query, _filter); } get _filter(): _Filter { const filters = this._queryConstraints.map(constraint => { if (isCompositeFilterConstraint(constraint)) { return constraint._filter; } return getFilterFromConstraint(constraint); }); return this.type === 'or' ? Filter.or(...filters) : Filter.and(...filters); } } export class QueryOrderByConstraint extends QueryConstraintBase { readonly type = 'orderBy'; constructor(fieldPath: string | FieldPath, directionStr?: OrderByDirection) { super(fieldPath, directionStr); } } export class QueryLimitConstraint extends QueryConstraintBase { readonly type: 'limit' | 'limitToLast'; constructor(type: 'limit' | 'limitToLast', limitValue: number) { super(limitValue); this.type = type; } } export class QueryStartAtConstraint extends QueryConstraintBase { readonly type: 'startAt' | 'startAfter'; constructor(type: 'startAt' | 'startAfter', ...docOrFields: Array) { super(...docOrFields); this.type = type; } } export class QueryEndAtConstraint extends QueryConstraintBase { readonly type: 'endAt' | 'endBefore'; constructor(type: 'endAt' | 'endBefore', ...fieldValues: unknown[]) { super(...fieldValues); this.type = type; } } export class QueryFieldFilterConstraint extends QueryConstraint { readonly type = 'where'; readonly _filter: _Filter; private readonly _applyFilterOnly: boolean; private readonly _fieldArgs?: [string | FieldPath, WhereFilterOp, unknown]; constructor( fieldPathOrFilter: string | FieldPath | _Filter, opStr?: WhereFilterOp, value?: unknown, ) { super(); if (fieldPathOrFilter instanceof _Filter && opStr === undefined && value === undefined) { this._filter = fieldPathOrFilter; this._applyFilterOnly = true; return; } this._filter = Filter(fieldPathOrFilter as string | FieldPath, opStr!, value); this._applyFilterOnly = false; this._fieldArgs = [fieldPathOrFilter as string | FieldPath, opStr!, value]; } _apply( query: Query, ): Query { const where = (query as unknown as QueryWithWhereInternal).where; if (this._applyFilterOnly) { return where.call(query, this._filter); } return where.apply(query, this._fieldArgs!); } } export type QueryFilterConstraint = QueryFieldFilterConstraint | QueryCompositeFilterConstraint; export type QueryNonFilterConstraint = | QueryOrderByConstraint | QueryLimitConstraint | QueryStartAtConstraint | QueryEndAtConstraint; export function query( queryRef: Query, compositeFilter: QueryCompositeFilterConstraint, ...queryConstraints: QueryNonFilterConstraint[] ): Query; export function query( queryRef: Query, ...queryConstraints: QueryConstraint[] ): Query; export function query( queryRef: Query, queryConstraint: QueryCompositeFilterConstraint | QueryConstraint | undefined, ...additionalQueryConstraints: Array ): Query { let queryConstraints: AppliableConstraint[] = []; if (queryConstraint instanceof AppliableConstraint) { queryConstraints.push(queryConstraint); } queryConstraints = queryConstraints.concat(additionalQueryConstraints); let constrainedQuery = queryRef; for (const constraint of queryConstraints) { if (!constraint) { continue; } constrainedQuery = constraint._apply(constrainedQuery); } return constrainedQuery; } export function where( fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown, ): QueryFieldFilterConstraint; export function where( fieldPath: string | FieldPath, opStr?: WhereFilterOp, value?: unknown, ): QueryFieldFilterConstraint { return new QueryFieldFilterConstraint(fieldPath as string | FieldPath | _Filter, opStr, value); } export function or(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint { return QueryCompositeFilterConstraint._create('or', queryConstraints); } export function and(...queryConstraints: QueryFilterConstraint[]): QueryCompositeFilterConstraint { return QueryCompositeFilterConstraint._create('and', queryConstraints); } export function orderBy( fieldPath: string | FieldPath, directionStr?: OrderByDirection, ): QueryOrderByConstraint { return new QueryOrderByConstraint(fieldPath, directionStr); } export function startAt< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(snapshot: DocumentSnapshot): QueryStartAtConstraint; export function startAt(...fieldValues: unknown[]): QueryStartAtConstraint; export function startAt(...docOrFields: Array): QueryStartAtConstraint { return new QueryStartAtConstraint('startAt', ...docOrFields); } export function startAfter< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(snapshot: DocumentSnapshot): QueryStartAtConstraint; export function startAfter(...fieldValues: unknown[]): QueryStartAtConstraint; export function startAfter( ...docOrFields: Array ): QueryStartAtConstraint { return new QueryStartAtConstraint('startAfter', ...docOrFields); } export function endAt( snapshot: DocumentSnapshot, ): QueryEndAtConstraint; export function endAt(...fieldValues: unknown[]): QueryEndAtConstraint; export function endAt(...args: unknown[]): QueryEndAtConstraint { return new QueryEndAtConstraint('endAt', ...args); } export function endBefore< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(snapshot: DocumentSnapshot): QueryEndAtConstraint; export function endBefore(...fieldValues: unknown[]): QueryEndAtConstraint; export function endBefore(...fieldValues: unknown[]): QueryEndAtConstraint { return new QueryEndAtConstraint('endBefore', ...fieldValues); } export function limit(limitValue: number): QueryLimitConstraint { return new QueryLimitConstraint('limit', limitValue); } export function limitToLast(limitValue: number): QueryLimitConstraint { return new QueryLimitConstraint('limitToLast', limitValue); } export function getDoc< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >( reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; return get.call(reference, { source: 'default' }); } export function getDocFromCache< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >( reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; return get.call(reference, { source: 'cache' }); } export function getDocFromServer< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >( reference: DocumentReference, ): Promise> { const get = (reference as unknown as DocumentReferenceGetInternal).get; return get.call(reference, { source: 'server' }); } export function getDocs< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; return get.call(queryRef, { source: 'default' }); } export function getDocsFromCache< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; return get.call(queryRef, { source: 'cache' }); } export function getDocsFromServer< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(queryRef: Query): Promise> { const get = (queryRef as unknown as QueryInternal).get; return get.call(queryRef, { source: 'server' }); } export function deleteDoc< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(reference: DocumentReference): Promise { const remove = (reference as unknown as DocumentReferenceDeleteInternal).delete; return remove.call(reference); } export function queryEqual< AppModelType = DocumentData, DbModelType extends DocumentData = DocumentData, >(left: Query, right: Query): boolean { const isEqual = (left as unknown as ReferenceIsEqualInternal).isEqual; return isEqual.call(left, right); }