react-native-firebase/lib/modules/firestore/Query.js

460 lines
12 KiB
JavaScript

/**
* @flow
* Query representation wrapper
*/
import DocumentSnapshot from './DocumentSnapshot';
import FieldPath from './FieldPath';
import QuerySnapshot from './QuerySnapshot';
import { buildNativeArray, buildTypeMap } from './utils/serialize';
import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import { getLogger } from '../../utils/log';
import { firestoreAutoId, isFunction, isObject } from '../../utils';
import { getNativeModule } from '../../utils/native';
import type Firestore from './';
import type Path from './Path';
import type {
QueryDirection,
QueryOperator,
QueryListenOptions,
} from './types';
const DIRECTIONS: { [QueryDirection]: string } = {
ASC: 'ASCENDING',
asc: 'ASCENDING',
DESC: 'DESCENDING',
desc: 'DESCENDING',
};
const OPERATORS: { [QueryOperator]: string } = {
'=': 'EQUAL',
'==': 'EQUAL',
'>': 'GREATER_THAN',
'>=': 'GREATER_THAN_OR_EQUAL',
'<': 'LESS_THAN',
'<=': 'LESS_THAN_OR_EQUAL',
};
type NativeFieldPath = {|
elements?: string[],
string?: string,
type: 'fieldpath' | 'string',
|};
type FieldFilter = {|
fieldPath: NativeFieldPath,
operator: string,
value: any,
|};
type FieldOrder = {|
direction: string,
fieldPath: NativeFieldPath,
|};
type QueryOptions = {
endAt?: any[],
endBefore?: any[],
limit?: number,
offset?: number,
selectFields?: string[],
startAfter?: any[],
startAt?: any[],
};
export type ObserverOnError = Object => void;
export type ObserverOnNext = QuerySnapshot => void;
export type Observer = {
error?: ObserverOnError,
next: ObserverOnNext,
};
const buildNativeFieldPath = (
fieldPath: string | FieldPath
): NativeFieldPath => {
if (fieldPath instanceof FieldPath) {
return {
elements: fieldPath._segments,
type: 'fieldpath',
};
}
return {
string: fieldPath,
type: 'string',
};
};
/**
* @class Query
*/
export default class Query {
_fieldFilters: FieldFilter[];
_fieldOrders: FieldOrder[];
_firestore: Firestore;
_iid: number;
_queryOptions: QueryOptions;
_referencePath: Path;
constructor(
firestore: Firestore,
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(): Firestore {
return this._firestore;
}
endAt(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
endAt: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
endBefore(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
endBefore: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
get(): Promise<QuerySnapshot> {
return getNativeModule(this._firestore)
.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(
optionsOrObserverOrOnNext: QueryListenOptions | Observer | ObserverOnNext,
observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError,
onError?: ObserverOnError
) {
let observer: Observer;
let queryListenOptions = {};
// Called with: onNext, ?onError
if (isFunction(optionsOrObserverOrOnNext)) {
if (observerOrOnNextOrOnError && !isFunction(observerOrOnNextOrOnError)) {
throw new Error(
'Query.onSnapshot failed: Second argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext,
error: observerOrOnNextOrOnError,
};
} else if (
optionsOrObserverOrOnNext &&
isObject(optionsOrObserverOrOnNext)
) {
// Called with: Observer
if (optionsOrObserverOrOnNext.next) {
if (isFunction(optionsOrObserverOrOnNext.next)) {
if (
optionsOrObserverOrOnNext.error &&
!isFunction(optionsOrObserverOrOnNext.error)
) {
throw new Error(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: optionsOrObserverOrOnNext.next,
error: optionsOrObserverOrOnNext.error,
};
} else {
throw new Error(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else if (
Object.prototype.hasOwnProperty.call(
optionsOrObserverOrOnNext,
'includeDocumentMetadataChanges'
) ||
Object.prototype.hasOwnProperty.call(
optionsOrObserverOrOnNext,
'includeQueryMetadataChanges'
)
) {
queryListenOptions = optionsOrObserverOrOnNext;
// Called with: Options, onNext, ?onError
if (isFunction(observerOrOnNextOrOnError)) {
if (onError && !isFunction(onError)) {
throw new Error(
'Query.onSnapshot failed: Third argument must be a valid function.'
);
}
// $FlowExpectedError: Not coping with the overloaded method signature
observer = {
next: observerOrOnNextOrOnError,
error: onError,
};
// Called with Options, Observer
} else if (
observerOrOnNextOrOnError &&
isObject(observerOrOnNextOrOnError) &&
observerOrOnNextOrOnError.next
) {
if (isFunction(observerOrOnNextOrOnError.next)) {
if (
observerOrOnNextOrOnError.error &&
!isFunction(observerOrOnNextOrOnError.error)
) {
throw new Error(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
}
observer = {
next: observerOrOnNextOrOnError.next,
error: observerOrOnNextOrOnError.error,
};
} else {
throw new Error(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: Second argument must be a function or observer.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: First argument must be a function, observer or options.'
);
}
} else {
throw new Error(
'Query.onSnapshot failed: Called with invalid arguments.'
);
}
const listenerId = firestoreAutoId();
const listener = nativeQuerySnapshot => {
const querySnapshot = new QuerySnapshot(
this._firestore,
this,
nativeQuerySnapshot
);
observer.next(querySnapshot);
};
// Listen to snapshot events
SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`),
listener
);
// Listen for snapshot error events
if (observer.error) {
SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`),
observer.error
);
}
// Add the native listener
getNativeModule(this._firestore).collectionOnSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId,
queryListenOptions
);
// Return an unsubscribe method
return this._offCollectionSnapshot.bind(this, listenerId, listener);
}
orderBy(
fieldPath: string | FieldPath,
directionStr?: QueryDirection = 'asc'
): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isOptionalFieldOrder('directionStr', directionStr);
if (
this._queryOptions.startAt ||
this._queryOptions.startAfter ||
this._queryOptions.endAt ||
this._queryOptions.endBefore
) {
throw new Error(
'Cannot specify an orderBy() constraint after calling ' +
'startAt(), startAfter(), endBefore() or endAt().'
);
}
const newOrder: FieldOrder = {
direction: DIRECTIONS[directionStr],
fieldPath: buildNativeFieldPath(fieldPath),
};
const combinedOrders = this._fieldOrders.concat(newOrder);
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
combinedOrders,
this._queryOptions
);
}
startAfter(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
startAfter: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
startAt(...snapshotOrVarArgs: any[]): Query {
const options = {
...this._queryOptions,
startAt: this._buildOrderByOption(snapshotOrVarArgs),
};
return new Query(
this.firestore,
this._referencePath,
this._fieldFilters,
this._fieldOrders,
options
);
}
where(
fieldPath: string | FieldPath,
opStr: QueryOperator,
value: any
): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isFieldFilter('fieldFilter', opStr, value);
const nativeValue = buildTypeMap(value);
const newFilter: FieldFilter = {
fieldPath: buildNativeFieldPath(fieldPath),
operator: OPERATORS[opStr],
value: nativeValue,
};
const combinedFilters = this._fieldFilters.concat(newFilter);
return new Query(
this.firestore,
this._referencePath,
combinedFilters,
this._fieldOrders,
this._queryOptions
);
}
/**
* INTERNALS
*/
_buildOrderByOption(snapshotOrVarArgs: any[]) {
// TODO: Validation
let values;
if (
snapshotOrVarArgs.length === 1 &&
snapshotOrVarArgs[0] instanceof DocumentSnapshot
) {
const docSnapshot: DocumentSnapshot = snapshotOrVarArgs[0];
values = [];
for (let i = 0; i < this._fieldOrders.length; i++) {
const fieldOrder = this._fieldOrders[i];
if (
fieldOrder.fieldPath.type === 'string' &&
fieldOrder.fieldPath.string
) {
values.push(docSnapshot.get(fieldOrder.fieldPath.string));
} else if (fieldOrder.fieldPath.fieldpath) {
const fieldPath = new FieldPath(...fieldOrder.fieldPath.fieldpath);
values.push(docSnapshot.get(fieldPath));
}
}
} else {
values = snapshotOrVarArgs;
}
return buildNativeArray(values);
}
/**
* Remove query snapshot listener
* @param listener
*/
_offCollectionSnapshot(listenerId: string, listener: Function) {
getLogger(this._firestore).info('Removing onQuerySnapshot listener');
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`),
listener
);
SharedEventEmitter.removeListener(
getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`),
listener
);
getNativeModule(this._firestore).collectionOffSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
);
}
}