Merge pull request #1509 from invertase/firestore-array-contains

[firestore] 'array-contains' query support
This commit is contained in:
Michael Diarmid
2018-09-20 17:12:05 +01:00
committed by GitHub
14 changed files with 1550 additions and 1409 deletions

View File

@@ -189,6 +189,9 @@ class RNFirebaseFirestoreCollectionReference {
case "LESS_THAN_OR_EQUAL":
query = query.whereLessThanOrEqualTo(fieldPath, value);
break;
case "ARRAY_CONTAINS":
query = query.whereArrayContains(fieldPath, value);
break;
}
} else {
ReadableArray fieldPathElements = fieldPathMap.getArray("elements");
@@ -213,6 +216,9 @@ class RNFirebaseFirestoreCollectionReference {
case "LESS_THAN_OR_EQUAL":
query = query.whereLessThanOrEqualTo(fieldPath, value);
break;
case "ARRAY_CONTAINS":
query = query.whereArrayContains(fieldPath, value);
break;
}
}
}

View File

@@ -6,254 +6,264 @@
static NSMutableDictionary *_listeners;
- (id)initWithPathAndModifiers:(RCTEventEmitter *) emitter
appDisplayName:(NSString *) appDisplayName
path:(NSString *) path
filters:(NSArray *) filters
orders:(NSArray *) orders
options:(NSDictionary *) options {
self = [super init];
if (self) {
_emitter = emitter;
_appDisplayName = appDisplayName;
_path = path;
_filters = filters;
_orders = orders;
_options = options;
_query = [self buildQuery];
}
// Initialise the static listeners object if required
if (!_listeners) {
_listeners = [[NSMutableDictionary alloc] init];
}
return self;
- (id)initWithPathAndModifiers:(RCTEventEmitter *)emitter
appDisplayName:(NSString *)appDisplayName
path:(NSString *)path
filters:(NSArray *)filters
orders:(NSArray *)orders
options:(NSDictionary *)options {
self = [super init];
if (self) {
_emitter = emitter;
_appDisplayName = appDisplayName;
_path = path;
_filters = filters;
_orders = orders;
_options = options;
_query = [self buildQuery];
}
// Initialise the static listeners object if required
if (!_listeners) {
_listeners = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)get:(NSDictionary *) getOptions
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject {
FIRFirestoreSource source;
if (getOptions && getOptions[@"source"]) {
if ([getOptions[@"source"] isEqualToString:@"server"]) {
source = FIRFirestoreSourceServer;
} else if ([getOptions[@"source"] isEqualToString:@"cache"]) {
source = FIRFirestoreSourceCache;
} else {
source = FIRFirestoreSourceDefault;
}
- (void)get:(NSDictionary *)getOptions
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject {
FIRFirestoreSource source;
if (getOptions && getOptions[@"source"]) {
if ([getOptions[@"source"] isEqualToString:@"server"]) {
source = FIRFirestoreSourceServer;
} else if ([getOptions[@"source"] isEqualToString:@"cache"]) {
source = FIRFirestoreSourceCache;
} else {
source = FIRFirestoreSourceDefault;
source = FIRFirestoreSourceDefault;
}
[_query getDocumentsWithSource:source completion:^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
[RNFirebaseFirestore promiseRejectException:reject error:error];
} else {
NSDictionary *data = [RNFirebaseFirestoreCollectionReference snapshotToDictionary:snapshot];
resolve(data);
}
}];
} else {
source = FIRFirestoreSourceDefault;
}
[_query getDocumentsWithSource:source completion:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
if (error) {
[RNFirebaseFirestore promiseRejectException:reject error:error];
} else {
NSDictionary *data = [RNFirebaseFirestoreCollectionReference snapshotToDictionary:snapshot];
resolve(data);
}
}];
}
+ (void)offSnapshot:(NSString *) listenerId {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
+ (void)offSnapshot:(NSString *)listenerId {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
}
- (void)onSnapshot:(NSString *) listenerId
queryListenOptions:(NSDictionary *) queryListenOptions {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRQuerySnapshot * _Nullable snapshot, NSError * _Nullable error) {
if (error) {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
[self handleQuerySnapshotError:listenerId error:error];
} else {
[self handleQuerySnapshotEvent:listenerId querySnapshot:snapshot];
}
};
bool includeMetadataChanges;
if (queryListenOptions && queryListenOptions[@"includeMetadataChanges"]) {
includeMetadataChanges = true;
} else {
includeMetadataChanges = false;
- (void)onSnapshot:(NSString *)listenerId
queryListenOptions:(NSDictionary *)queryListenOptions {
if (_listeners[listenerId] == nil) {
id listenerBlock = ^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
if (error) {
id<FIRListenerRegistration> listener = _listeners[listenerId];
if (listener) {
[_listeners removeObjectForKey:listenerId];
[listener remove];
}
[self handleQuerySnapshotError:listenerId error:error];
} else {
[self handleQuerySnapshotEvent:listenerId querySnapshot:snapshot];
}
};
id<FIRListenerRegistration> listener = [_query addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges listener:listenerBlock];
_listeners[listenerId] = listener;
bool includeMetadataChanges;
if (queryListenOptions && queryListenOptions[@"includeMetadataChanges"]) {
includeMetadataChanges = true;
} else {
includeMetadataChanges = false;
}
id<FIRListenerRegistration>
listener = [_query addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges listener:listenerBlock];
_listeners[listenerId] = listener;
}
}
- (FIRQuery *)buildQuery {
FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:_appDisplayName];
FIRQuery *query = (FIRQuery*)[firestore collectionWithPath:_path];
query = [self applyFilters:firestore query:query];
query = [self applyOrders:query];
query = [self applyOptions:firestore query:query];
FIRFirestore *firestore = [RNFirebaseFirestore getFirestoreForApp:_appDisplayName];
FIRQuery *query = (FIRQuery *) [firestore collectionWithPath:_path];
query = [self applyFilters:firestore query:query];
query = [self applyOrders:query];
query = [self applyOptions:firestore query:query];
return query;
return query;
}
- (FIRQuery *)applyFilters:(FIRFirestore *) firestore
query:(FIRQuery *) query {
for (NSDictionary *filter in _filters) {
NSDictionary *fieldPathDictionary = filter[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
NSString *operator = filter[@"operator"];
NSDictionary *jsValue = filter[@"value"];
id value = [RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:jsValue];
- (FIRQuery *)applyFilters:(FIRFirestore *)firestore
query:(FIRQuery *)query {
for (NSDictionary *filter in _filters) {
NSDictionary *fieldPathDictionary = filter[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
NSString *operator = filter[@"operator"];
NSDictionary *jsValue = filter[@"value"];
id value = [RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:jsValue];
if ([fieldPathType isEqualToString:@"string"]) {
NSString *fieldPath = fieldPathDictionary[@"string"];
if ([operator isEqualToString:@"EQUAL"]) {
query = [query queryWhereField:fieldPath isEqualTo:value];
} else if ([operator isEqualToString:@"GREATER_THAN"]) {
query = [query queryWhereField:fieldPath isGreaterThan:value];
} else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
query = [query queryWhereField:fieldPath isGreaterThanOrEqualTo:value];
} else if ([operator isEqualToString:@"LESS_THAN"]) {
query = [query queryWhereField:fieldPath isLessThan:value];
} else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
query = [query queryWhereField:fieldPath isLessThanOrEqualTo:value];
}
} else {
NSArray *fieldPathElements = fieldPathDictionary[@"elements"];
FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathElements];
if ([operator isEqualToString:@"EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isEqualTo:value];
} else if ([operator isEqualToString:@"GREATER_THAN"]) {
query = [query queryWhereFieldPath:fieldPath isGreaterThan:value];
} else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value];
} else if ([operator isEqualToString:@"LESS_THAN"]) {
query = [query queryWhereFieldPath:fieldPath isLessThan:value];
} else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isLessThanOrEqualTo:value];
}
}
if ([fieldPathType isEqualToString:@"string"]) {
NSString *fieldPath = fieldPathDictionary[@"string"];
if ([operator isEqualToString:@"EQUAL"]) {
query = [query queryWhereField:fieldPath isEqualTo:value];
} else if ([operator isEqualToString:@"GREATER_THAN"]) {
query = [query queryWhereField:fieldPath isGreaterThan:value];
} else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
query = [query queryWhereField:fieldPath isGreaterThanOrEqualTo:value];
} else if ([operator isEqualToString:@"LESS_THAN"]) {
query = [query queryWhereField:fieldPath isLessThan:value];
} else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
query = [query queryWhereField:fieldPath isLessThanOrEqualTo:value];
} else if ([operator isEqualToString:@"ARRAY_CONTAINS"]) {
query = [query queryWhereField:fieldPath arrayContains:value];
}
} else {
NSArray *fieldPathElements = fieldPathDictionary[@"elements"];
FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathElements];
if ([operator isEqualToString:@"EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isEqualTo:value];
} else if ([operator isEqualToString:@"GREATER_THAN"]) {
query = [query queryWhereFieldPath:fieldPath isGreaterThan:value];
} else if ([operator isEqualToString:@"GREATER_THAN_OR_EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isGreaterThanOrEqualTo:value];
} else if ([operator isEqualToString:@"LESS_THAN"]) {
query = [query queryWhereFieldPath:fieldPath isLessThan:value];
} else if ([operator isEqualToString:@"LESS_THAN_OR_EQUAL"]) {
query = [query queryWhereFieldPath:fieldPath isLessThanOrEqualTo:value];
} else if ([operator isEqualToString:@"ARRAY_CONTAINS"]) {
query = [query queryWhereFieldPath:fieldPath arrayContains:value];
}
}
return query;
}
return query;
}
- (FIRQuery *)applyOrders:(FIRQuery *) query {
for (NSDictionary *order in _orders) {
NSString *direction = order[@"direction"];
NSDictionary *fieldPathDictionary = order[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
- (FIRQuery *)applyOrders:(FIRQuery *)query {
for (NSDictionary *order in _orders) {
NSString *direction = order[@"direction"];
NSDictionary *fieldPathDictionary = order[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
if ([fieldPathType isEqualToString:@"string"]) {
NSString *fieldPath = fieldPathDictionary[@"string"];
query = [query queryOrderedByField:fieldPath descending:([direction isEqualToString:@"DESCENDING"])];
} else {
NSArray *fieldPathElements = fieldPathDictionary[@"elements"];
FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathElements];
query = [query queryOrderedByFieldPath:fieldPath descending:([direction isEqualToString:@"DESCENDING"])];
}
if ([fieldPathType isEqualToString:@"string"]) {
NSString *fieldPath = fieldPathDictionary[@"string"];
query = [query queryOrderedByField:fieldPath descending:([direction isEqualToString:@"DESCENDING"])];
} else {
NSArray *fieldPathElements = fieldPathDictionary[@"elements"];
FIRFieldPath *fieldPath = [[FIRFieldPath alloc] initWithFields:fieldPathElements];
query = [query queryOrderedByFieldPath:fieldPath descending:([direction isEqualToString:@"DESCENDING"])];
}
return query;
}
return query;
}
- (FIRQuery *)applyOptions:(FIRFirestore *) firestore
query:(FIRQuery *) query {
if (_options[@"endAt"]) {
query = [query queryEndingAtValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"endAt"]]];
}
if (_options[@"endBefore"]) {
query = [query queryEndingBeforeValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"endBefore"]]];
}
if (_options[@"limit"]) {
query = [query queryLimitedTo:[_options[@"limit"] intValue]];
}
if (_options[@"offset"]) {
// iOS doesn't support offset
}
if (_options[@"selectFields"]) {
// iOS doesn't support selectFields
}
if (_options[@"startAfter"]) {
query = [query queryStartingAfterValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"startAfter"]]];
}
if (_options[@"startAt"]) {
query = [query queryStartingAtValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"startAt"]]];
}
return query;
- (FIRQuery *)applyOptions:(FIRFirestore *)firestore
query:(FIRQuery *)query {
if (_options[@"endAt"]) {
query =
[query queryEndingAtValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"endAt"]]];
}
if (_options[@"endBefore"]) {
query =
[query queryEndingBeforeValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"endBefore"]]];
}
if (_options[@"limit"]) {
query = [query queryLimitedTo:[_options[@"limit"] intValue]];
}
if (_options[@"offset"]) {
// iOS doesn't support offset
}
if (_options[@"selectFields"]) {
// iOS doesn't support selectFields
}
if (_options[@"startAfter"]) {
query =
[query queryStartingAfterValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"startAfter"]]];
}
if (_options[@"startAt"]) {
query =
[query queryStartingAtValues:[RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:_options[@"startAt"]]];
}
return query;
}
- (void)handleQuerySnapshotError:(NSString *)listenerId
error:(NSError *)error {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_appDisplayName forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"];
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_appDisplayName forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestore getJSError:error] forKey:@"error"];
[RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
[RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
}
- (void)handleQuerySnapshotEvent:(NSString *)listenerId
querySnapshot:(FIRQuerySnapshot *)querySnapshot {
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_appDisplayName forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"];
NSMutableDictionary *event = [[NSMutableDictionary alloc] init];
[event setValue:_appDisplayName forKey:@"appName"];
[event setValue:_path forKey:@"path"];
[event setValue:listenerId forKey:@"listenerId"];
[event setValue:[RNFirebaseFirestoreCollectionReference snapshotToDictionary:querySnapshot] forKey:@"querySnapshot"];
[RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
[RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_COLLECTION_SYNC_EVENT body:event];
}
+ (NSDictionary *)snapshotToDictionary:(FIRQuerySnapshot *)querySnapshot {
NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init];
[snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"];
[snapshot setValue:[self documentSnapshotsToArray:querySnapshot.documents] forKey:@"documents"];
if (querySnapshot.metadata) {
NSMutableDictionary *metadata = [[NSMutableDictionary alloc] init];
[metadata setValue:@(querySnapshot.metadata.fromCache) forKey:@"fromCache"];
[metadata setValue:@(querySnapshot.metadata.hasPendingWrites) forKey:@"hasPendingWrites"];
[snapshot setValue:metadata forKey:@"metadata"];
}
NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init];
[snapshot setValue:[self documentChangesToArray:querySnapshot.documentChanges] forKey:@"changes"];
[snapshot setValue:[self documentSnapshotsToArray:querySnapshot.documents] forKey:@"documents"];
if (querySnapshot.metadata) {
NSMutableDictionary *metadata = [[NSMutableDictionary alloc] init];
[metadata setValue:@(querySnapshot.metadata.fromCache) forKey:@"fromCache"];
[metadata setValue:@(querySnapshot.metadata.hasPendingWrites) forKey:@"hasPendingWrites"];
[snapshot setValue:metadata forKey:@"metadata"];
}
return snapshot;
return snapshot;
}
+ (NSArray *)documentChangesToArray:(NSArray<FIRDocumentChange *> *) documentChanges {
NSMutableArray *changes = [[NSMutableArray alloc] init];
for (FIRDocumentChange *change in documentChanges) {
[changes addObject:[self documentChangeToDictionary:change]];
}
+ (NSArray *)documentChangesToArray:(NSArray<FIRDocumentChange *> *)documentChanges {
NSMutableArray *changes = [[NSMutableArray alloc] init];
for (FIRDocumentChange *change in documentChanges) {
[changes addObject:[self documentChangeToDictionary:change]];
}
return changes;
return changes;
}
+ (NSDictionary *)documentChangeToDictionary:(FIRDocumentChange *)documentChange {
NSMutableDictionary *change = [[NSMutableDictionary alloc] init];
[change setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentChange.document] forKey:@"document"];
[change setValue:@(documentChange.newIndex) forKey:@"newIndex"];
[change setValue:@(documentChange.oldIndex) forKey:@"oldIndex"];
NSMutableDictionary *change = [[NSMutableDictionary alloc] init];
[change setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentChange.document] forKey:@"document"];
[change setValue:@(documentChange.newIndex) forKey:@"newIndex"];
[change setValue:@(documentChange.oldIndex) forKey:@"oldIndex"];
if (documentChange.type == FIRDocumentChangeTypeAdded) {
[change setValue:@"added" forKey:@"type"];
} else if (documentChange.type == FIRDocumentChangeTypeRemoved) {
[change setValue:@"removed" forKey:@"type"];
} else if (documentChange.type == FIRDocumentChangeTypeModified) {
[change setValue:@"modified" forKey:@"type"];
}
if (documentChange.type == FIRDocumentChangeTypeAdded) {
[change setValue:@"added" forKey:@"type"];
} else if (documentChange.type == FIRDocumentChangeTypeRemoved) {
[change setValue:@"removed" forKey:@"type"];
} else if (documentChange.type == FIRDocumentChangeTypeModified) {
[change setValue:@"modified" forKey:@"type"];
}
return change;
return change;
}
+ (NSArray *)documentSnapshotsToArray:(NSArray<FIRDocumentSnapshot *> *) documentSnapshots {
NSMutableArray *snapshots = [[NSMutableArray alloc] init];
for (FIRDocumentSnapshot *snapshot in documentSnapshots) {
[snapshots addObject:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]];
}
+ (NSArray *)documentSnapshotsToArray:(NSArray<FIRDocumentSnapshot *> *)documentSnapshots {
NSMutableArray *snapshots = [[NSMutableArray alloc] init];
for (FIRDocumentSnapshot *snapshot in documentSnapshots) {
[snapshots addObject:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]];
}
return snapshots;
return snapshots;
}
#endif

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-firebase",
"version": "5.0.0-rc2",
"version": "5.0.0-rc3",
"author": "Invertase <oss@invertase.io> (http://invertase.io)",
"description": "A well tested, feature rich Firebase implementation for React Native, supporting iOS & Android. Individual module support for Admob, Analytics, Auth, Crash Reporting, Cloud Firestore, Database, Dynamic Links, Functions, Messaging (FCM), Remote Config, Storage and more.",
"main": "dist/index.js",

2
src/index.d.ts vendored
View File

@@ -2670,7 +2670,7 @@ declare module 'react-native-firebase' {
}
type QueryDirection = 'asc' | 'ASC' | 'desc' | 'DESC';
type QueryOperator = '=' | '==' | '>' | '>=' | '<' | '<=';
type QueryOperator = '=' | '==' | '>' | '>=' | '<' | '<=' | 'array-contains';
interface TypeMap {
type:

View File

@@ -7,7 +7,6 @@ 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';
@@ -34,6 +33,7 @@ const OPERATORS: { [QueryOperator]: string } = {
'>=': 'GREATER_THAN_OR_EQUAL',
'<': 'LESS_THAN',
'<=': 'LESS_THAN_OR_EQUAL',
'array-contains': 'ARRAY_CONTAINS',
};
type NativeFieldPath = {|
@@ -200,6 +200,7 @@ export default class Query {
observerOrOnNextOrOnError?: Observer | ObserverOnNext | ObserverOnError,
onError?: ObserverOnError
) {
// TODO refactor this 💩
let observer: Observer;
let metadataChanges = {};
// Called with: onNext, ?onError
@@ -309,14 +310,15 @@ export default class Query {
};
// Listen to snapshot events
SharedEventEmitter.addListener(
const snapshotSubscription = SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshot:${listenerId}`),
listener
);
// Listen for snapshot error events
let errorSubscription;
if (observer.error) {
SharedEventEmitter.addListener(
errorSubscription = SharedEventEmitter.addListener(
getAppEventName(this._firestore, `onQuerySnapshotError:${listenerId}`),
observer.error
);
@@ -332,8 +334,19 @@ export default class Query {
metadataChanges
);
// Return an unsubscribe method
return this._offCollectionSnapshot.bind(this, listenerId, listener);
// return an unsubscribe method
return () => {
snapshotSubscription.remove();
if (errorSubscription) errorSubscription.remove();
// cancel native listener
getNativeModule(this._firestore).collectionOffSnapshot(
this._referencePath.relativeName,
this._fieldFilters,
this._fieldOrders,
this._queryOptions,
listenerId
);
};
}
orderBy(
@@ -414,6 +427,7 @@ export default class Query {
operator: OPERATORS[opStr],
value: nativeValue,
};
const combinedFilters = this._fieldFilters.concat(newFilter);
return new Query(
this.firestore,
@@ -455,27 +469,4 @@ export default class Query {
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
);
}
}

View File

@@ -8,7 +8,14 @@ export type MetadataChanges = {|
export type QueryDirection = 'DESC' | 'desc' | 'ASC' | 'asc';
export type QueryOperator = '<' | '<=' | '=' | '==' | '>' | '>=';
export type QueryOperator =
| '<'
| '<='
| '='
| '=='
| '>'
| '>='
| 'array-contains';
export type GetOptions = {
source: 'default' | 'server' | 'cache',

View File

@@ -0,0 +1,532 @@
const {
COL_DOC_1,
cleanCollection,
TEST_COLLECTION_NAME_DYNAMIC,
} = TestHelpers.firestore;
describe('firestore()', () => {
before(async () => {
await cleanCollection(TEST_COLLECTION_NAME_DYNAMIC);
const collection = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
await Promise.all([
collection.doc('col1').set({ ...COL_DOC_1(), foo: 'bar0' }),
collection.doc('col2').set({
...COL_DOC_1(),
foo: 'bar1',
daz: 234,
object: { daz: 234 },
timestamp: new jet.context.window.Date(2017, 2, 11, 10, 0, 0),
}),
collection.doc('col3').set({
...COL_DOC_1(),
foo: 'bar2',
daz: 345,
object: { daz: 345 },
timestamp: new jet.context.window.Date(2017, 2, 12, 10, 0, 0),
}),
collection.doc('col4').set({
...COL_DOC_1(),
foo: 'bar3',
daz: 456,
object: { daz: 456 },
timestamp: new jet.context.window.Date(2017, 2, 13, 10, 0, 0),
}),
collection.doc('col5').set({
...COL_DOC_1(),
foo: 'bar4',
daz: 567,
object: { daz: 567 },
timestamp: new jet.context.window.Date(2017, 2, 14, 10, 0, 0),
}),
]);
});
describe('CollectionReference', () => {
describe('cursors', () => {
describe('endAt', () => {
it('handles dates', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('timestamp')
.endAt(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
}));
it('handles numbers', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('daz')
.endAt(345)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
}));
it('handles strings', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endAt('bar2')
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
}));
it('handles snapshots', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endAt(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
});
});
it('works with FieldPath', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('timestamp'))
.endAt(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
}));
it('handles snapshots with FieldPath', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('foo'))
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endAt(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
345,
]);
});
});
});
describe('endBefore', () => {
it('handles dates', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('timestamp')
.endBefore(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
}));
it('handles numbers', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('daz')
.endBefore(345)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
}));
it('handles strings', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endBefore('bar2')
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
}));
it('handles snapshots', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endBefore(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
});
});
it('works with FieldPath', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('timestamp'))
.endBefore(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
}));
it('handles snapshots with FieldPath', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('foo'))
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.endBefore(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
123,
234,
]);
});
});
});
describe('startAt', () => {
it('handles dates', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('timestamp')
.startAt(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
}));
it('handles numbers', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('daz')
.startAt(345)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
}));
it('handles strings', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAt('bar2')
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
}));
it('handles snapshots', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAt(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
});
});
it('works with FieldPath', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('timestamp'))
.startAt(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
}));
it('handles snapshots with FieldPath', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('foo'))
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAt(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
345,
456,
567,
]);
});
});
});
describe('startAfter', () => {
it('handles dates', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('timestamp')
.startAfter(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
}));
it('handles numbers', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('daz')
.startAfter(345)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
}));
it('handles strings', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAfter('bar2')
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
}));
it('handles snapshot', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAfter(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
});
});
it('works with FieldPath', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('timestamp'))
.startAfter(new jet.context.window.Date(2017, 2, 12, 10, 0, 0))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
}));
it('handles snapshots with FieldPath', async () => {
const collectionSnapshot = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy(new firebase.firestore.FieldPath('foo'))
.get();
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.orderBy('foo')
.startAfter(collectionSnapshot.docs[2])
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 2);
should.deepEqual(querySnapshot.docs.map(doc => doc.data().daz), [
456,
567,
]);
});
});
});
describe('orderBy()', () => {
it('errors if called after startAt', () => {
(() => {
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.startAt({})
.orderBy('test');
}).should.throw(
'Cannot specify an orderBy() constraint after calling startAt(), startAfter(), endBefore() or endAt().'
);
});
it('errors if called after startAfter', () => {
(() => {
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.startAfter({})
.orderBy('test');
}).should.throw(
'Cannot specify an orderBy() constraint after calling startAt(), startAfter(), endBefore() or endAt().'
);
});
it('errors if called after endBefore', () => {
(() => {
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.endBefore({})
.orderBy('test');
}).should.throw(
'Cannot specify an orderBy() constraint after calling startAt(), startAfter(), endBefore() or endAt().'
);
});
it('errors if called after endAt', () => {
(() => {
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.endAt({})
.orderBy('test');
}).should.throw(
'Cannot specify an orderBy() constraint after calling startAt(), startAfter(), endBefore() or endAt().'
);
});
});
});
});
});

View File

@@ -0,0 +1,61 @@
const {
COL_DOC_1,
cleanCollection,
TEST_COLLECTION_NAME_DYNAMIC,
} = TestHelpers.firestore;
describe('firestore()', () => {
before(async () => {
await cleanCollection(TEST_COLLECTION_NAME_DYNAMIC);
const collection = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
await Promise.all([
collection.doc('col1').set(COL_DOC_1()),
collection.doc('col2').set({ ...COL_DOC_1(), daz: 2 }),
collection.doc('col3').set({ ...COL_DOC_1(), daz: 3 }),
collection.doc('col4').set({ ...COL_DOC_1(), daz: 4 }),
collection.doc('col5').set({ ...COL_DOC_1(), daz: 5 }),
]);
});
describe('CollectionReference', () => {
describe('limit()', () => {
it('correctly works with get()', async () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.limit(3)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 3);
}));
it('correctly works with onSnapshot()', async () => {
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.limit(3);
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
unsubscribe = collectionRef.onSnapshot(snapshot => {
callback(snapshot.size);
resolve2();
});
});
// Assertions
callback.should.be.calledWith(3);
// Tear down
unsubscribe();
});
});
});
});

View File

@@ -0,0 +1,520 @@
const {
COL_DOC_1,
DOC_2_PATH,
COL_DOC_1_PATH,
cleanCollection,
resetTestCollectionDoc,
TEST_COLLECTION_NAME_DYNAMIC,
} = TestHelpers.firestore;
describe('firestore()', () => {
describe('CollectionReference', () => {
describe('onSnapshot()', () => {
beforeEach(async () => {
await sleep(50);
await cleanCollection(TEST_COLLECTION_NAME_DYNAMIC);
await sleep(50);
});
it('QuerySnapshot has correct properties', async () => {
const collection = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const snapshot = await collection.get();
snapshot.docChanges.should.be.an.Array();
snapshot.empty.should.equal(true);
snapshot.metadata.should.be.an.Object();
snapshot.query.should.be.an.Object();
});
it('DocumentChange has correct properties', async () => {
const collection = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
await resetTestCollectionDoc();
// Test
let changes;
let unsubscribe;
await new Promise(resolve => {
unsubscribe = collection.onSnapshot(snapshot => {
changes = snapshot.docChanges;
resolve();
});
});
// Assertions
changes.should.be.a.Array();
changes[0].doc.should.be.an.Object();
changes[0].newIndex.should.be.a.Number();
changes[0].oldIndex.should.be.a.Number();
changes[0].type.should.be.a.String();
// Tear down
unsubscribe();
});
it('calls callback with the initial data and then when document changes', async () => {
const callback = sinon.spy();
const collection = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...COL_DOC_1(), foo: 'updated' };
// Test
let unsubscribe;
let resolved = false;
await new Promise(resolve => {
unsubscribe = collection.onSnapshot(snapshot => {
if (snapshot && snapshot.docs.length) {
callback(snapshot.docs[0].data());
} else {
callback(null);
}
if (!resolved) {
resolved = true;
resolve();
}
});
});
callback.should.be.calledOnce();
await firebase
.firestore()
.doc(COL_DOC_1_PATH)
.set(newDocValue);
await sleep(25);
// Assertions
callback.should.be.calledTwice();
callback.getCall(1).args[0].foo.should.equal('updated');
// Tear down
unsubscribe();
});
// crappy race condition somewhere =/ will come back to it later
it('calls callback with the initial data and then when document is added', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { foo: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
unsubscribe = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
});
});
callback.should.be.calledWith(colDoc);
const docRef = firebase.firestore().doc(DOC_2_PATH);
await docRef.set(newDocValue);
await sleep(25);
// Assertions
callback.should.be.calledWith(colDoc);
callback.should.be.calledWith(newDocValue);
callback.should.be.calledThrice();
// Tear down
unsubscribe();
});
it("doesn't call callback when the ref is updated with the same value", async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
unsubscribe = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => {
callback(doc.data());
});
resolve2();
});
});
callback.should.be.calledWith(colDoc);
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(colDoc);
await sleep(150);
// Assertions
callback.should.be.calledOnce(); // Callback is not called again
// Tear down
unsubscribe();
});
it('allows binding multiple callbacks to the same ref', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
// Setup
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...colDoc, foo: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise(resolve2 => {
unsubscribeA = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise(resolve2 => {
unsubscribeB = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(colDoc);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(colDoc);
callbackB.should.be.calledOnce();
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(newDocValue);
await sleep(25);
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Tear down
unsubscribeA();
unsubscribeB();
});
it('listener stops listening when unsubscribed', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
// Setup
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...colDoc, foo: 'updated' };
const callbackA = sinon.spy();
const callbackB = sinon.spy();
// Test
let unsubscribeA;
let unsubscribeB;
await new Promise(resolve2 => {
unsubscribeA = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => callbackA(doc.data()));
resolve2();
});
});
await new Promise(resolve2 => {
unsubscribeB = collectionRef.onSnapshot(snapshot => {
snapshot.forEach(doc => callbackB(doc.data()));
resolve2();
});
});
callbackA.should.be.calledWith(colDoc);
callbackA.should.be.calledOnce();
callbackB.should.be.calledWith(colDoc);
callbackB.should.be.calledOnce();
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(newDocValue);
await sleep(25);
callbackA.should.be.calledWith(newDocValue);
callbackB.should.be.calledWith(newDocValue);
callbackA.should.be.calledTwice();
callbackB.should.be.calledTwice();
// Unsubscribe A
unsubscribeA();
await docRef.set(colDoc);
await sleep(25);
callbackB.should.be.calledWith(colDoc);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
// Unsubscribe B
unsubscribeB();
await docRef.set(newDocValue);
await sleep(25);
callbackA.should.be.calledTwice();
callbackB.should.be.calledThrice();
});
it('supports options and callback', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...colDoc, foo: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
unsubscribe = collectionRef.onSnapshot(
{
includeMetadataChanges: true,
},
snapshot => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
}
);
});
callback.should.be.calledWith(colDoc);
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(newDocValue);
await sleep(25);
// Assertions
callback.should.be.calledWith(newDocValue);
// Tear down
unsubscribe();
});
it('supports observer', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...colDoc, foo: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
const observer = {
next: snapshot => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
},
};
unsubscribe = collectionRef.onSnapshot(observer);
});
callback.should.be.calledWith(colDoc);
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(newDocValue);
await sleep(25);
// Assertions
callback.should.be.calledWith(newDocValue);
callback.should.be.calledTwice();
// Tear down
unsubscribe();
});
it('supports options and observer', async () => {
const colDoc = await resetTestCollectionDoc();
await sleep(50);
const collectionRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
const newDocValue = { ...colDoc, foo: 'updated' };
const callback = sinon.spy();
// Test
let unsubscribe;
await new Promise(resolve2 => {
const observer = {
next: snapshot => {
snapshot.forEach(doc => callback(doc.data()));
resolve2();
},
error: () => {},
};
unsubscribe = collectionRef.onSnapshot(
{
includeMetadataChanges: true,
},
observer
);
});
callback.should.be.calledWith(colDoc);
const docRef = firebase.firestore().doc(COL_DOC_1_PATH);
await docRef.set(newDocValue);
await sleep(25);
// Assertions
callback.should.be.calledWith(newDocValue);
// Tear down
unsubscribe();
});
it('errors when invalid parameters supplied', async () => {
const colRef = firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC);
(() => {
colRef.onSnapshot(() => {}, 'error');
}).should.throw(
'Query.onSnapshot failed: Second argument must be a valid function.'
);
(() => {
colRef.onSnapshot({
next: () => {},
error: 'error',
});
}).should.throw(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
(() => {
colRef.onSnapshot({
next: 'error',
});
}).should.throw(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
(() => {
colRef.onSnapshot(
{
includeMetadataChanges: true,
},
() => {},
'error'
);
}).should.throw(
'Query.onSnapshot failed: Third argument must be a valid function.'
);
(() => {
colRef.onSnapshot(
{
includeMetadataChanges: true,
},
{
next: () => {},
error: 'error',
}
);
}).should.throw(
'Query.onSnapshot failed: Observer.error must be a valid function.'
);
(() => {
colRef.onSnapshot(
{
includeMetadataChanges: true,
},
{
next: 'error',
}
);
}).should.throw(
'Query.onSnapshot failed: Observer.next must be a valid function.'
);
(() => {
colRef.onSnapshot(
{
includeMetadataChanges: true,
},
'error'
);
}).should.throw(
'Query.onSnapshot failed: Second argument must be a function or observer.'
);
(() => {
colRef.onSnapshot({
error: 'error',
});
}).should.throw(
'Query.onSnapshot failed: First argument must be a function, observer or options.'
);
(() => {
colRef.onSnapshot();
}).should.throw(
'Query.onSnapshot failed: Called with invalid arguments.'
);
});
});
});
});

View File

@@ -0,0 +1,171 @@
const {
COL_DOC_1,
COL_DOC_1_PATH,
TEST_COLLECTION_NAME_DYNAMIC,
resetTestCollectionDoc,
} = TestHelpers.firestore;
describe('firestore()', () => {
describe('CollectionReference', () => {
before(() => resetTestCollectionDoc(COL_DOC_1_PATH, COL_DOC_1()));
describe('where()', () => {
it('`array-contains` a string value', async () => {
const found = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('arrString', 'array-contains', 'a')
.get();
should.equal(found.size, 1);
found.forEach(documentSnapshot => {
should.deepEqual(
documentSnapshot.data().arrString,
jet.contextify(['a', 'b', 'c', 'd'])
);
});
const notFound = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('arrString', 'array-contains', 'f')
.get();
should.equal(notFound.size, 0);
});
it('`array-contains` a number value', async () => {
const found = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('arrNumber', 'array-contains', 1)
.get();
should.equal(found.size, 1);
found.forEach(documentSnapshot => {
should.deepEqual(
documentSnapshot.data().arrNumber,
jet.contextify([1, 2, 3, 4])
);
});
const notFound = await firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('arrNumber', 'array-contains', 5)
.get();
should.equal(notFound.size, 0);
});
// TODO: below tests should also check the inverse to ensure working as
// TODO: currently there is only one document in the collection so might be false positives in future
it('== boolean value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('baz', '==', true)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().baz, true);
});
}));
it('== string value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('foo', '==', 'bar')
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().foo, 'bar');
});
}));
it('== null value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('naz', '==', null)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().naz, null);
});
}));
it('== date value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('timestamp', '==', COL_DOC_1().timestamp)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
}));
it('== GeoPoint value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('geopoint', '==', COL_DOC_1().geopoint)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
}));
it('>= number value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('daz', '>=', 123)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().daz, 123);
});
}));
it('>= GeoPoint value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('geopoint', '>=', new firebase.firestore.GeoPoint(-1, -1))
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
}));
it('<= float value', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where('gaz', '<=', 12.1234666)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().gaz, 12.1234567);
});
}));
it('FieldPath', () =>
firebase
.firestore()
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.where(new firebase.firestore.FieldPath('baz'), '==', true)
.get()
.then(querySnapshot => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach(documentSnapshot => {
should.equal(documentSnapshot.data().baz, true);
});
}));
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ const {
COL2_DOC_1,
COL2_DOC_1_ID,
COL2_DOC_1_PATH,
TEST2_COLLECTION_NAME,
TEST_COLLECTION_NAME_DYNAMIC,
resetTestCollectionDoc,
} = TestHelpers.firestore;
@@ -31,7 +31,7 @@ describe('firestore()', () => {
describe('parent', () => {
it('should return parent collection', () => {
const document = test2DocRef(COL2_DOC_1_ID);
document.parent.id.should.equal(TEST2_COLLECTION_NAME);
document.parent.id.should.equal(TEST_COLLECTION_NAME_DYNAMIC);
});
});

View File

@@ -1,5 +1,7 @@
const TEST_COLLECTION_NAME = 'tests';
const TEST2_COLLECTION_NAME = 'tests2';
const TEST_COLLECTION_NAME_DYNAMIC = `tests${Math.floor(Math.random() * 30) +
1}`;
// const TEST3_COLLECTION_NAME = 'tests3';
let shouldCleanup = false;
@@ -11,33 +13,36 @@ module.exports = {
await Promise.all([
module.exports.cleanCollection(TEST_COLLECTION_NAME),
module.exports.cleanCollection(TEST2_COLLECTION_NAME),
module.exports.cleanCollection(TEST_COLLECTION_NAME_DYNAMIC),
]);
// await module.exports.cleanCollection(`${TEST_COLLECTION_NAME}3`);
// await module.exports.cleanCollection(`${TEST_COLLECTION_NAME}4`);
return Promise.resolve();
},
TEST_COLLECTION_NAME,
TEST2_COLLECTION_NAME,
TEST_COLLECTION_NAME_DYNAMIC,
// TEST3_COLLECTION_NAME,
DOC_1: { name: 'doc1' },
DOC_1_PATH: `tests/doc1${testRunId}`,
DOC_1_PATH: `${TEST_COLLECTION_NAME_DYNAMIC}/doc1${testRunId}`,
DOC_2: { name: 'doc2', title: 'Document 2' },
DOC_2_PATH: `tests/doc2${testRunId}`,
DOC_2_PATH: `${TEST_COLLECTION_NAME_DYNAMIC}/doc2${testRunId}`,
// needs to be a fn as firebase may not yet be available
COL_DOC_1() {
shouldCleanup = true;
return {
testRunId,
baz: true,
daz: 123,
foo: 'bar',
gaz: 12.1234567,
geopoint: new firebase.firestore.GeoPoint(0, 0),
naz: null,
arrNumber: [1, 2, 3, 4],
arrString: ['a', 'b', 'c', 'd'],
object: {
daz: 123,
},
@@ -55,6 +60,8 @@ module.exports = {
gaz: 12.1234567,
geopoint: new firebase.firestore.GeoPoint(0, 0),
naz: null,
arrNumber: [1, 2, 3, 4],
arrString: ['a', 'b', 'c', 'd'],
object: {
daz: 123,
},
@@ -63,10 +70,10 @@ module.exports = {
},
COL_DOC_1_ID: `col1${testRunId}`,
COL_DOC_1_PATH: `${TEST_COLLECTION_NAME}/col1${testRunId}`,
COL_DOC_1_PATH: `${TEST_COLLECTION_NAME_DYNAMIC}/col1${testRunId}`,
COL2_DOC_1_ID: `doc1${testRunId}`,
COL2_DOC_1_PATH: `${TEST2_COLLECTION_NAME}/doc1${testRunId}`,
COL2_DOC_1_PATH: `${TEST_COLLECTION_NAME_DYNAMIC}/doc1${testRunId}`,
/**
* Removes all documents on the collection for the current testId or
@@ -76,7 +83,7 @@ module.exports = {
* @return {Promise<*>}
*/
async cleanCollection(collectionName) {
const firestore = firebaseAdmin.firestore();
const firestore = firebase.firestore();
const collection = firestore.collection(
collectionName || TEST_COLLECTION_NAME
);
@@ -92,7 +99,8 @@ module.exports = {
if (
ref.path.includes(testRunId) ||
new Date(docsToDelete[i].createTime) <= yesterday
new Date(docsToDelete[i].createTime) <= yesterday ||
collectionName === TEST_COLLECTION_NAME_DYNAMIC
) {
batch.delete(ref);
}
@@ -109,7 +117,7 @@ module.exports = {
shouldCleanup = true;
return firebase
.firestore()
.collection(TEST_COLLECTION_NAME)
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.doc(
docId.startsWith(testRunId) || docId.endsWith(testRunId)
? docId
@@ -121,7 +129,7 @@ module.exports = {
shouldCleanup = true;
return firebase
.firestore()
.collection(TEST2_COLLECTION_NAME)
.collection(TEST_COLLECTION_NAME_DYNAMIC)
.doc(
docId.startsWith(testRunId) || docId.endsWith(testRunId)
? docId
@@ -147,7 +155,7 @@ module.exports = {
async resetTestCollectionDoc(path, doc) {
shouldCleanup = true;
const _doc = doc || module.exports.COL_DOC_1();
await module.exports.cleanCollection(TEST_COLLECTION_NAME_DYNAMIC);
await firebase
.firestore()
.doc(path || module.exports.COL_DOC_1_PATH)

View File

@@ -236,7 +236,7 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNFirebase (5.0.0-rc2):
- RNFirebase (5.0.0-rc3):
- Firebase/Core
- React
- yoga (0.57.0.React)