/** * @flow * Query representation wrapper */ import DocumentSnapshot from './DocumentSnapshot'; import Path from './Path'; import QuerySnapshot from './QuerySnapshot'; import INTERNALS from '../../internals'; import { firestoreAutoId } from '../../utils'; const DIRECTIONS = { ASC: 'ASCENDING', asc: 'ASCENDING', DESC: 'DESCENDING', desc: 'DESCENDING', }; const OPERATORS = { '=': 'EQUAL', '==': 'EQUAL', '>': 'GREATER_THAN', '>=': 'GREATER_THAN_OR_EQUAL', '<': 'LESS_THAN', '<=': 'LESS_THAN_OR_EQUAL', }; export type Direction = 'DESC' | 'desc' | 'ASC' | 'asc'; type FieldFilter = { fieldPath: string, operator: string, value: any, } type FieldOrder = { direction: string, fieldPath: string, } type QueryOptions = { endAt?: any[], endBefore?: any[], limit?: number, offset?: number, selectFields?: string[], startAfter?: any[], startAt?: any[], } export type Operator = '<' | '<=' | '=' | '==' | '>' | '>='; /** * @class Query */ export default class Query { _fieldFilters: FieldFilter[]; _fieldOrders: FieldOrder[]; _firestore: Object; _iid: number; _queryOptions: QueryOptions; _referencePath: Path; constructor(firestore: Object, path: Path, fieldFilters?: FieldFilter[], fieldOrders?: FieldOrder[], queryOptions?: QueryOptions) { this._fieldFilters = fieldFilters || []; this._fieldOrders = fieldOrders || []; this._firestore = firestore; this._queryOptions = queryOptions || {}; this._referencePath = path; } get firestore(): Object { return this._firestore; } endAt(fieldValues: any): Query { fieldValues = [].slice.call(arguments); // TODO: Validation const options = { ...this._queryOptions, endAt: fieldValues, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, this._fieldOrders, options); } endBefore(fieldValues: any): Query { fieldValues = [].slice.call(arguments); // TODO: Validation const options = { ...this._queryOptions, endBefore: fieldValues, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, this._fieldOrders, options); } get(): Promise { return this._firestore._native .collectionGet( this._referencePath.relativeName, this._fieldFilters, this._fieldOrders, this._queryOptions, ) .then(nativeData => new QuerySnapshot(this._firestore, this, nativeData)); } limit(limit: number): Query { // TODO: Validation // validate.isInteger('n', n); const options = { ...this._queryOptions, limit, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, this._fieldOrders, options); } onSnapshot(onNext: () => any, onError?: () => any): () => void { // TODO: Validation const listenerId = firestoreAutoId(); const listener = (nativeQuerySnapshot) => { const querySnapshot = new QuerySnapshot(this._firestore, this, nativeQuerySnapshot); onNext(querySnapshot); }; // Listen to snapshot events this._firestore.on( this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), listener, ); // Listen for snapshot error events if (onError) { this._firestore.on( this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), onError, ); } // Add the native listener this._firestore._native .collectionOnSnapshot( this._referencePath.relativeName, this._fieldFilters, this._fieldOrders, this._queryOptions, listenerId ); // Return an unsubscribe method return this._offCollectionSnapshot.bind(this, listenerId, listener); } orderBy(fieldPath: string, directionStr?: Direction = 'asc'): Query { // TODO: Validation // validate.isFieldPath('fieldPath', fieldPath); // validate.isOptionalFieldOrder('directionStr', directionStr); if (this._queryOptions.startAt || this._queryOptions.endAt) { throw new Error('Cannot specify an orderBy() constraint after calling ' + 'startAt(), startAfter(), endBefore() or endAt().'); } const newOrder = { direction: DIRECTIONS[directionStr], fieldPath, }; const combinedOrders = this._fieldOrders.concat(newOrder); return new Query(this.firestore, this._referencePath, this._fieldFilters, combinedOrders, this._queryOptions); } startAfter(fieldValues: any): Query { fieldValues = [].slice.call(arguments); // TODO: Validation const options = { ...this._queryOptions, startAfter: fieldValues, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, this._fieldOrders, options); } startAt(fieldValues: any): Query { fieldValues = [].slice.call(arguments); // TODO: Validation const options = { ...this._queryOptions, startAt: fieldValues, }; return new Query(this.firestore, this._referencePath, this._fieldFilters, this._fieldOrders, options); } where(fieldPath: string, opStr: Operator, value: any): Query { // TODO: Validation // validate.isFieldPath('fieldPath', fieldPath); // validate.isFieldFilter('fieldFilter', opStr, value); const newFilter = { fieldPath, operator: OPERATORS[opStr], value, }; const combinedFilters = this._fieldFilters.concat(newFilter); return new Query(this.firestore, this._referencePath, combinedFilters, this._fieldOrders, this._queryOptions); } /** * Remove query snapshot listener * @param listener */ _offCollectionSnapshot(listenerId: number, listener: Function) { this._firestore.log.info('Removing onQuerySnapshot listener'); this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshot:${listenerId}`), listener); this._firestore.removeListener(this._firestore._getAppEventName(`onQuerySnapshotError:${listenerId}`), listener); this._firestore._native .collectionOffSnapshot( this._referencePath.relativeName, this._fieldFilters, this._fieldOrders, this._queryOptions, listenerId ); } }