[firestore] Add FieldPath support to DocumentSnapshot and Query

This commit is contained in:
Chris Bianca 2018-01-11 18:28:14 +00:00
parent d700bf9d6d
commit 5e062868fc
13 changed files with 327 additions and 50 deletions

View File

@ -12,6 +12,7 @@ import com.facebook.react.bridge.WritableMap;
import com.google.firebase.firestore.DocumentChange;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FieldPath;
import com.google.firebase.firestore.FieldValue;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.GeoPoint;
@ -269,6 +270,8 @@ public class FirestoreSerialize {
} else if ("date".equals(type)) {
Double time = typeMap.getDouble("value");
return new Date(time.longValue());
} else if ("documentid".equals(type)) {
return FieldPath.documentId();
} else if ("fieldvalue".equals(type)) {
String value = typeMap.getString("value");
if ("delete".equals(value)) {

View File

@ -14,6 +14,7 @@ import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.firestore.DocumentListenOptions;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FieldPath;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration;
@ -21,6 +22,7 @@ import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QueryListenOptions;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -128,27 +130,56 @@ public class RNFirebaseFirestoreCollectionReference {
private Query applyFilters(FirebaseFirestore firestore, Query query) {
for (int i = 0; i < filters.size(); i++) {
ReadableMap filter = filters.getMap(i);
String fieldPath = filter.getString("fieldPath");
ReadableMap fieldPathMap = filter.getMap("fieldPath");
String fieldPathType = fieldPathMap.getString("type");
String operator = filter.getString("operator");
ReadableMap jsValue = filter.getMap("value");
Object value = FirestoreSerialize.parseTypeMap(firestore, jsValue);
switch (operator) {
case "EQUAL":
query = query.whereEqualTo(fieldPath, value);
break;
case "GREATER_THAN":
query = query.whereGreaterThan(fieldPath, value);
break;
case "GREATER_THAN_OR_EQUAL":
query = query.whereGreaterThanOrEqualTo(fieldPath, value);
break;
case "LESS_THAN":
query = query.whereLessThan(fieldPath, value);
break;
case "LESS_THAN_OR_EQUAL":
query = query.whereLessThanOrEqualTo(fieldPath, value);
break;
if (fieldPathType.equals("string")) {
String fieldPath = fieldPathMap.getString("string");
switch (operator) {
case "EQUAL":
query = query.whereEqualTo(fieldPath, value);
break;
case "GREATER_THAN":
query = query.whereGreaterThan(fieldPath, value);
break;
case "GREATER_THAN_OR_EQUAL":
query = query.whereGreaterThanOrEqualTo(fieldPath, value);
break;
case "LESS_THAN":
query = query.whereLessThan(fieldPath, value);
break;
case "LESS_THAN_OR_EQUAL":
query = query.whereLessThanOrEqualTo(fieldPath, value);
break;
}
} else {
ReadableArray fieldPathElements = fieldPathMap.getArray("elements");
String[] fieldPathArray = new String[fieldPathElements.size()];
for (int j=0; j<fieldPathElements.size(); j++) {
fieldPathArray[j] = fieldPathElements.getString(j);
}
FieldPath fieldPath = FieldPath.of(fieldPathArray);
switch (operator) {
case "EQUAL":
query = query.whereEqualTo(fieldPath, value);
break;
case "GREATER_THAN":
query = query.whereGreaterThan(fieldPath, value);
break;
case "GREATER_THAN_OR_EQUAL":
query = query.whereGreaterThanOrEqualTo(fieldPath, value);
break;
case "LESS_THAN":
query = query.whereLessThan(fieldPath, value);
break;
case "LESS_THAN_OR_EQUAL":
query = query.whereLessThanOrEqualTo(fieldPath, value);
break;
}
}
}
return query;
@ -159,9 +190,17 @@ public class RNFirebaseFirestoreCollectionReference {
for (Object o : ordersList) {
Map<String, Object> order = (Map) o;
String direction = (String) order.get("direction");
String fieldPath = (String) order.get("fieldPath");
Map<String, Object> fieldPathMap = (Map) order.get("fieldPath");
String fieldPathType = (String)fieldPathMap.get("type");
query = query.orderBy(fieldPath, Query.Direction.valueOf(direction));
if (fieldPathType.equals("string")) {
String fieldPath = (String)fieldPathMap.get("string");
query = query.orderBy(fieldPath, Query.Direction.valueOf(direction));
} else {
List<String> fieldPathElements = (List)fieldPathMap.get("elements");
FieldPath fieldPath = FieldPath.of(fieldPathElements.toArray(new String[fieldPathElements.size()]));
query = query.orderBy(fieldPath, Query.Direction.valueOf(direction));
}
}
return query;
}

View File

@ -93,21 +93,39 @@ queryListenOptions:(NSDictionary *) queryListenOptions {
- (FIRQuery *)applyFilters:(FIRFirestore *) firestore
query:(FIRQuery *) query {
for (NSDictionary *filter in _filters) {
NSString *fieldPath = filter[@"fieldPath"];
NSDictionary *fieldPathDictionary = filter[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
NSString *operator = filter[@"operator"];
NSDictionary *jsValue = filter[@"value"];
id value = [RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:jsValue];
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];
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];
}
}
}
return query;
@ -116,9 +134,17 @@ queryListenOptions:(NSDictionary *) queryListenOptions {
- (FIRQuery *)applyOrders:(FIRQuery *) query {
for (NSDictionary *order in _orders) {
NSString *direction = order[@"direction"];
NSString *fieldPath = order[@"fieldPath"];
NSDictionary *fieldPathDictionary = order[@"fieldPath"];
NSString *fieldPathType = fieldPathDictionary[@"type"];
query = [query queryOrderedByField: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;
}

View File

@ -255,6 +255,8 @@ static NSMutableDictionary *_listeners;
return [[FIRGeoPoint alloc] initWithLatitude:[latitude doubleValue] longitude:[longitude doubleValue]];
} else if ([type isEqualToString:@"date"]) {
return [NSDate dateWithTimeIntervalSince1970:([(NSNumber *)value doubleValue] / 1000.0)];
} else if ([type isEqualToString:@"documentid"]) {
return [FIRFieldPath documentID];
} else if ([type isEqualToString:@"fieldvalue"]) {
NSString *string = (NSString*)value;
if ([string isEqualToString:@"delete"]) {

View File

@ -8,6 +8,7 @@ import { firestoreAutoId } from '../../utils';
import type Firestore from './';
import type { FirestoreQueryDirection, FirestoreQueryOperator } from '../../types';
import type FieldPath from './FieldPath';
import type Path from './Path';
import type { Observer, ObserverOnError, ObserverOnNext, QueryListenOptions } from './Query';
import type QuerySnapshot from './QuerySnapshot';
@ -81,7 +82,7 @@ export default class CollectionReference {
return this._query.onSnapshot(optionsOrObserverOrOnNext, observerOrOnNextOrOnError, onError);
}
orderBy(fieldPath: string, directionStr?: FirestoreQueryDirection): Query {
orderBy(fieldPath: string | FieldPath, directionStr?: FirestoreQueryDirection): Query {
return this._query.orderBy(fieldPath, directionStr);
}

View File

@ -3,12 +3,25 @@
* DocumentSnapshot representation wrapper
*/
import DocumentReference from './DocumentReference';
import FieldPath from './FieldPath';
import Path from './Path';
import { isObject } from '../../utils';
import { parseNativeMap } from './utils/serialize';
import type Firestore from './';
import type { FirestoreNativeDocumentSnapshot, FirestoreSnapshotMetadata } from '../../types';
const extractFieldPathData = (data: Object | void, segments: string[]): any => {
if (!data || !isObject(data)) {
return undefined;
}
const pathValue = data[segments[0]];
if (segments.length === 1) {
return pathValue;
}
return extractFieldPathData(pathValue, segments.slice(1));
};
/**
* @class DocumentSnapshot
*/
@ -43,7 +56,10 @@ export default class DocumentSnapshot {
return this._data;
}
get(fieldPath: string): any {
get(fieldPath: string | FieldPath): any {
if (fieldPath instanceof FieldPath) {
return extractFieldPathData(this._data, fieldPath._segments);
}
return this._data ? this._data[fieldPath] : undefined;
}
}

View File

@ -0,0 +1,22 @@
/**
* @flow
* FieldPath representation wrapper
*/
/**
* @class FieldPath
*/
export default class FieldPath {
_segments: string[];
constructor(...segments: string[]) {
// TODO: Validation
this._segments = segments;
}
static documentId(): FieldPath {
return DOCUMENT_ID;
}
}
export const DOCUMENT_ID = new FieldPath('__name__');

View File

@ -3,6 +3,7 @@
* 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';
@ -30,15 +31,20 @@ const OPERATORS: { [FirestoreQueryOperator]: string } = {
'<=': 'LESS_THAN_OR_EQUAL',
};
type FieldFilter = {
fieldPath: string,
type NativeFieldPath = {|
elements?: string[],
string?: string,
type: 'fieldpath' | 'string',
|}
type FieldFilter = {|
fieldPath: NativeFieldPath,
operator: string,
value: any,
}
type FieldOrder = {
|}
type FieldOrder = {|
direction: string,
fieldPath: string,
}
fieldPath: NativeFieldPath,
|}
type QueryOptions = {
endAt?: any[],
endBefore?: any[],
@ -49,10 +55,10 @@ type QueryOptions = {
startAt?: any[],
}
export type QueryListenOptions = {
export type QueryListenOptions = {|
includeDocumentMetadataChanges: boolean,
includeQueryMetadataChanges: boolean,
}
|}
export type ObserverOnError = (Object) => void;
export type ObserverOnNext = (QuerySnapshot) => void;
@ -62,6 +68,19 @@ export type Observer = {
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
*/
@ -252,7 +271,7 @@ export default class Query {
return this._offCollectionSnapshot.bind(this, listenerId, listener);
}
orderBy(fieldPath: string, directionStr?: FirestoreQueryDirection = 'asc'): Query {
orderBy(fieldPath: string | FieldPath, directionStr?: FirestoreQueryDirection = 'asc'): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isOptionalFieldOrder('directionStr', directionStr);
@ -262,9 +281,9 @@ export default class Query {
'startAt(), startAfter(), endBefore() or endAt().');
}
const newOrder = {
const newOrder: FieldOrder = {
direction: DIRECTIONS[directionStr],
fieldPath,
fieldPath: buildNativeFieldPath(fieldPath),
};
const combinedOrders = this._fieldOrders.concat(newOrder);
return new Query(
@ -306,13 +325,13 @@ export default class Query {
);
}
where(fieldPath: string, opStr: FirestoreQueryOperator, value: any): Query {
where(fieldPath: string | FieldPath, opStr: FirestoreQueryOperator, value: any): Query {
// TODO: Validation
// validate.isFieldPath('fieldPath', fieldPath);
// validate.isFieldFilter('fieldFilter', opStr, value);
const nativeValue = buildTypeMap(value);
const newFilter = {
fieldPath,
const newFilter: FieldFilter = {
fieldPath: buildNativeFieldPath(fieldPath),
operator: OPERATORS[opStr],
value: nativeValue,
};
@ -334,11 +353,16 @@ export default class Query {
// TODO: Validation
let values;
if (snapshotOrVarArgs.length === 1 && snapshotOrVarArgs[0] instanceof DocumentSnapshot) {
const docSnapshot = snapshotOrVarArgs[0];
const docSnapshot: DocumentSnapshot = snapshotOrVarArgs[0];
values = [];
for (let i = 0; i < this._fieldOrders.length; i++) {
const fieldOrder = this._fieldOrders[i];
values.push(docSnapshot.get(fieldOrder.fieldPath));
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;

View File

@ -8,6 +8,7 @@ import { getAppEventName, SharedEventEmitter } from '../../utils/events';
import ModuleBase from '../../utils/ModuleBase';
import CollectionReference from './CollectionReference';
import DocumentReference from './DocumentReference';
import FieldPath from './FieldPath';
import FieldValue from './FieldValue';
import GeoPoint from './GeoPoint';
import Path from './Path';
@ -148,6 +149,7 @@ export default class Firestore extends ModuleBase {
}
export const statics = {
FieldPath,
FieldValue,
GeoPoint,
enableLogging(enabled: boolean) {

View File

@ -3,6 +3,7 @@
*/
import DocumentReference from '../DocumentReference';
import { DOCUMENT_ID } from '../FieldPath';
import { DELETE_FIELD_VALUE, SERVER_TIMESTAMP_FIELD_VALUE } from '../FieldValue';
import GeoPoint from '../GeoPoint';
import Path from '../Path';
@ -60,6 +61,10 @@ export const buildTypeMap = (value: any): FirestoreTypeMap | null => {
type: 'fieldvalue',
value: 'timestamp',
};
} else if (value === DOCUMENT_ID) {
return {
type: 'documentid',
};
} else if (type === 'boolean' || type === 'number' || type === 'string') {
return {
type,

View File

@ -500,6 +500,19 @@ function collectionReferenceTests({ describe, it, context, firebase, before, aft
});
});
});
it('correctly handles FieldPath', () => {
return firebase.native.firestore()
.collection('collection-tests')
.where(new firebase.native.firestore.FieldPath('baz'), '==', true)
.get()
.then((querySnapshot) => {
should.equal(querySnapshot.size, 1);
querySnapshot.forEach((documentSnapshot) => {
should.equal(documentSnapshot.data().baz, true);
});
});
});
});
context('limit', () => {
@ -614,6 +627,31 @@ function collectionReferenceTests({ describe, it, context, firebase, before, aft
);
});
});
it('works with FieldPath', () => {
return collectionTests.orderBy(new firebase.native.firestore.FieldPath('timestamp')).endAt(new 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 collectionTests.orderBy(new firebase.native.firestore.FieldPath('foo')).get();
return collectionTests.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],
);
});
});
});
context('endBefore', () => {
@ -665,6 +703,31 @@ function collectionReferenceTests({ describe, it, context, firebase, before, aft
);
});
});
it('works with FieldPath', () => {
return collectionTests.orderBy(new firebase.native.firestore.FieldPath('timestamp')).endBefore(new 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 collectionTests.orderBy(new firebase.native.firestore.FieldPath('foo')).get();
return collectionTests.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],
);
});
});
});
context('startAt', () => {
@ -716,6 +779,31 @@ function collectionReferenceTests({ describe, it, context, firebase, before, aft
);
});
});
it('works with FieldPath', () => {
return collectionTests.orderBy(new firebase.native.firestore.FieldPath('timestamp')).startAt(new 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 collectionTests.orderBy(new firebase.native.firestore.FieldPath('foo')).get();
return collectionTests.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],
);
});
});
});
context('startAfter', () => {
@ -767,6 +855,31 @@ function collectionReferenceTests({ describe, it, context, firebase, before, aft
);
});
});
it('works with FieldPath', () => {
return collectionTests.orderBy(new firebase.native.firestore.FieldPath('timestamp')).startAfter(new 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 collectionTests.orderBy(new firebase.native.firestore.FieldPath('foo')).get();
return collectionTests.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],
);
});
});
});
context('onSnapshot()', () => {

View File

@ -0,0 +1,22 @@
import should from 'should';
function fieldPathTests({ describe, it, context, firebase }) {
describe('FieldPath', () => {
context('DocumentSnapshot.get()', () => {
it('should get the correct values', () => {
return firebase.native.firestore()
.doc('collection-tests/col1')
.get()
.then((snapshot) => {
should.equal(snapshot.get('foo'), 'bar');
should.equal(snapshot.get(new firebase.native.firestore.FieldPath('foo')), 'bar');
should.equal(snapshot.get(new firebase.native.firestore.FieldPath('object', 'daz')), 123);
should.equal(snapshot.get(new firebase.native.firestore.FieldPath('nonexistent', 'object')), undefined);
});
});
});
});
}
export default fieldPathTests;

View File

@ -6,6 +6,7 @@ import TestSuite from '../../../lib/TestSuite';
*/
import collectionReferenceTests from './collectionReferenceTests';
import documentReferenceTests from './documentReferenceTests';
import fieldPathTests from './fieldPathTests';
import fieldValueTests from './fieldValueTests';
import firestoreTests from './firestoreTests';
@ -30,6 +31,7 @@ const suite = new TestSuite('Firestore', 'firebase.firestore()', firebase);
const testGroups = [
collectionReferenceTests,
documentReferenceTests,
fieldPathTests,
fieldValueTests,
firestoreTests,
];