diff --git a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java index 7945a766..d5568d77 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -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.FieldValue; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.QuerySnapshot; @@ -273,6 +274,16 @@ public class FirestoreSerialize { Log.e(TAG, "parseTypeMap", exception); return null; } + } else if ("fieldvalue".equals(type)) { + String value = typeMap.getString("value"); + if ("delete".equals(value)) { + return FieldValue.delete(); + } else if ("timestamp".equals(value)) { + return FieldValue.serverTimestamp(); + } else { + Log.e(TAG, "parseTypeMap: Invalid fieldvalue: " + value); + return null; + } } else { Log.e(TAG, "parseTypeMap: Cannot convert object of type " + type); return null; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index e27b3349..bc2aa448 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -260,7 +260,7 @@ static NSMutableDictionary *_listeners; } else if ([type isEqualToString:@"reference"]) { return [firestore documentWithPath:value]; } else if ([type isEqualToString:@"geopoint"]) { - NSDictionary* geopoint = (NSDictionary*)value; + NSDictionary *geopoint = (NSDictionary*)value; NSNumber *latitude = geopoint[@"latitude"]; NSNumber *longitude = geopoint[@"longitude"]; return [[FIRGeoPoint alloc] initWithLatitude:[latitude doubleValue] longitude:[longitude doubleValue]]; @@ -268,6 +268,16 @@ static NSMutableDictionary *_listeners; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; return [dateFormatter dateFromString:value]; + } else if ([type isEqualToString:@"fieldvalue"]) { + NSString *string = (NSString*)value; + if ([string isEqualToString:@"delete"]) { + return [FIRFieldValue fieldValueForDelete]; + } else if ([string isEqualToString:@"timestamp"]) { + return [FIRFieldValue fieldValueForServerTimestamp]; + } else { + // TODO: Log warning + return nil; + } } else if ([type isEqualToString:@"boolean"] || [type isEqualToString:@"number"] || [type isEqualToString:@"string"] || [type isEqualToString:@"null"]) { return value; } else { diff --git a/lib/modules/firestore/FieldValue.js b/lib/modules/firestore/FieldValue.js new file mode 100644 index 00000000..14067038 --- /dev/null +++ b/lib/modules/firestore/FieldValue.js @@ -0,0 +1,17 @@ +/** + * @flow + * FieldValue representation wrapper + */ + +export default class FieldValue { + static delete(): FieldValue { + return DELETE_FIELD_VALUE; + } + + static serverTimestamp(): FieldValue { + return SERVER_TIMESTAMP_FIELD_VALUE; + } +} + +export const DELETE_FIELD_VALUE = new FieldValue(); +export const SERVER_TIMESTAMP_FIELD_VALUE = new FieldValue(); diff --git a/lib/modules/firestore/index.js b/lib/modules/firestore/index.js index 5d593f1b..b24223aa 100644 --- a/lib/modules/firestore/index.js +++ b/lib/modules/firestore/index.js @@ -2,12 +2,11 @@ * @flow * Firestore representation wrapper */ -import { NativeModules } from 'react-native'; - import ModuleBase from './../../utils/ModuleBase'; import CollectionReference from './CollectionReference'; import DocumentReference from './DocumentReference'; import DocumentSnapshot from './DocumentSnapshot'; +import FieldValue from './FieldValue'; import GeoPoint from './GeoPoint'; import Path from './Path'; import WriteBatch from './WriteBatch'; @@ -137,9 +136,6 @@ export default class Firestore extends ModuleBase { } export const statics = { - FieldValue: { - delete: () => NativeModules.RNFirebaseFirestore && NativeModules.RNFirebaseFirestore.deleteFieldValue || {}, - serverTimestamp: () => NativeModules.RNFirebaseFirestore && NativeModules.RNFirebaseFirestore.serverTimestampFieldValue || {} - }, + FieldValue, GeoPoint, }; diff --git a/lib/modules/firestore/utils/serialize.js b/lib/modules/firestore/utils/serialize.js index 129165ba..6015848b 100644 --- a/lib/modules/firestore/utils/serialize.js +++ b/lib/modules/firestore/utils/serialize.js @@ -1,6 +1,7 @@ // @flow import DocumentReference from '../DocumentReference'; +import { DELETE_FIELD_VALUE, SERVER_TIMESTAMP_FIELD_VALUE } from '../FieldValue'; import GeoPoint from '../GeoPoint'; import Path from '../Path'; import { typeOf } from '../../../utils'; @@ -42,6 +43,12 @@ const buildTypeMap = (value: any): any => { if (value === null) { typeMap.type = 'null'; typeMap.value = null; + } else if (value === DELETE_FIELD_VALUE) { + typeMap.type = 'fieldvalue'; + typeMap.value = 'delete'; + } else if (value === SERVER_TIMESTAMP_FIELD_VALUE) { + typeMap.type = 'fieldvalue'; + typeMap.value = 'timestamp'; } else if (type === 'boolean' || type === 'number' || type === 'string') { typeMap.type = type; typeMap.value = value; @@ -99,7 +106,9 @@ const parseNativeArray = (firestore: Object, nativeArray: Object[]): any[] => { const parseTypeMap = (firestore: Object, typeMap: TypeMap): any => { const { type, value } = typeMap; - if (type === 'boolean' || type === 'number' || type === 'string' || type === 'null') { + if (type === 'null') { + return null; + } else if (type === 'boolean' || type === 'number' || type === 'string') { return value; } else if (type === 'array') { return parseNativeArray(firestore, value); diff --git a/tests/src/tests/firestore/collectionReferenceTests.js b/tests/src/tests/firestore/collectionReferenceTests.js index 1145c03d..a2fe0300 100644 --- a/tests/src/tests/firestore/collectionReferenceTests.js +++ b/tests/src/tests/firestore/collectionReferenceTests.js @@ -2,6 +2,8 @@ import sinon from 'sinon'; import 'should-sinon'; import should from 'should'; +import { COL_1 } from './index'; + function collectionReferenceTests({ describe, it, context, firebase }) { describe('CollectionReference', () => { context('class', () => { @@ -54,9 +56,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('calls callback with the initial data and then when document changes', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callback = sinon.spy(); @@ -70,9 +71,9 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -92,9 +93,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('calls callback with the initial data and then when document is added', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { foo: 'updated' }; const callback = sinon.spy(); @@ -108,9 +108,9 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc2'); + const docRef = firebase.native.firestore().doc('collection-tests/col2'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -119,7 +119,7 @@ function collectionReferenceTests({ describe, it, context, firebase }) { // Assertions - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); callback.should.be.calledWith(newDocValue); callback.should.be.calledThrice(); @@ -131,8 +131,7 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('doesn\'t call callback when the ref is updated with the same value', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); const callback = sinon.spy(); @@ -146,10 +145,10 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); - await docRef.set(currentDocValue); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); + await docRef.set(COL_1); await new Promise((resolve2) => { setTimeout(() => resolve2(), 5); @@ -168,9 +167,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('allows binding multiple callbacks to the same ref', async () => { // Setup - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callbackA = sinon.spy(); const callbackB = sinon.spy(); @@ -191,13 +189,13 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callbackA.should.be.calledWith(currentDocValue); + callbackA.should.be.calledWith(COL_1); callbackA.should.be.calledOnce(); - callbackB.should.be.calledWith(currentDocValue); + callbackB.should.be.calledWith(COL_1); callbackB.should.be.calledOnce(); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -220,9 +218,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('listener stops listening when unsubscribed', async () => { // Setup - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callbackA = sinon.spy(); const callbackB = sinon.spy(); @@ -243,13 +240,13 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callbackA.should.be.calledWith(currentDocValue); + callbackA.should.be.calledWith(COL_1); callbackA.should.be.calledOnce(); - callbackB.should.be.calledWith(currentDocValue); + callbackB.should.be.calledWith(COL_1); callbackB.should.be.calledOnce(); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -266,13 +263,13 @@ function collectionReferenceTests({ describe, it, context, firebase }) { unsubscribeA(); - await docRef.set(currentDocValue); + await docRef.set(COL_1); await new Promise((resolve2) => { setTimeout(() => resolve2(), 5); }); - callbackB.should.be.calledWith(currentDocValue); + callbackB.should.be.calledWith(COL_1); callbackA.should.be.calledTwice(); callbackB.should.be.calledThrice(); @@ -294,9 +291,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('supports options and callback', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callback = sinon.spy(); @@ -310,9 +306,9 @@ function collectionReferenceTests({ describe, it, context, firebase }) { }); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -331,9 +327,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('supports observer', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callback = sinon.spy(); @@ -350,9 +345,9 @@ function collectionReferenceTests({ describe, it, context, firebase }) { unsubscribe = collectionRef.onSnapshot(observer); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { @@ -372,9 +367,8 @@ function collectionReferenceTests({ describe, it, context, firebase }) { context('onSnapshot()', () => { it('supports options and observer', async () => { - const collectionRef = firebase.native.firestore().collection('document-tests'); - const currentDocValue = { name: 'doc1' }; - const newDocValue = { name: 'updated' }; + const collectionRef = firebase.native.firestore().collection('collection-tests'); + const newDocValue = { ...COL_1, foo: 'updated' }; const callback = sinon.spy(); @@ -391,9 +385,9 @@ function collectionReferenceTests({ describe, it, context, firebase }) { unsubscribe = collectionRef.onSnapshot({ includeQueryMetadataChanges: true, includeDocumentMetadataChanges: true }, observer); }); - callback.should.be.calledWith(currentDocValue); + callback.should.be.calledWith(COL_1); - const docRef = firebase.native.firestore().doc('document-tests/doc1'); + const docRef = firebase.native.firestore().doc('collection-tests/col1'); await docRef.set(newDocValue); await new Promise((resolve2) => { diff --git a/tests/src/tests/firestore/fieldValueTests.js b/tests/src/tests/firestore/fieldValueTests.js new file mode 100644 index 00000000..df59665b --- /dev/null +++ b/tests/src/tests/firestore/fieldValueTests.js @@ -0,0 +1,36 @@ +import should from 'should'; + + +function fieldValueTests({ describe, it, context, firebase }) { + describe('FieldValue', () => { + context('delete()', () => { + it('should delete field', () => { + return firebase.native.firestore() + .doc('document-tests/doc2') + .update({ + title: firebase.native.firestore.FieldValue.delete(), + }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc2').get(); + should.equal(doc.data().title, undefined); + }); + }); + }); + + context('serverTimestamp()', () => { + it('should set timestamp', () => { + return firebase.native.firestore() + .doc('document-tests/doc2') + .update({ + creationDate: firebase.native.firestore.FieldValue.serverTimestamp(), + }) + .then(async () => { + const doc = await firebase.native.firestore().doc('document-tests/doc2').get(); + doc.data().creationDate.should.be.instanceof(Date); + }); + }); + }); + }); +} + +export default fieldValueTests; diff --git a/tests/src/tests/firestore/index.js b/tests/src/tests/firestore/index.js index 22e4b734..2801340f 100644 --- a/tests/src/tests/firestore/index.js +++ b/tests/src/tests/firestore/index.js @@ -6,13 +6,26 @@ import TestSuite from '../../../lib/TestSuite'; */ import collectionReferenceTests from './collectionReferenceTests'; import documentReferenceTests from './documentReferenceTests'; +import fieldValueTests from './fieldValueTests'; import firestoreTests from './firestoreTests'; +export const COL_1 = { + baz: true, + daz: 123, + foo: 'bar', + gaz: 12.1234567, + naz: null, +}; + +export const DOC_1 = { name: 'doc1' }; +export const DOC_2 = { name: 'doc2', title: 'Document 2' }; + const suite = new TestSuite('Firestore', 'firebase.firestore()', firebase); const testGroups = [ collectionReferenceTests, documentReferenceTests, + fieldValueTests, firestoreTests, ]; @@ -21,28 +34,22 @@ function firestoreTestSuite(testSuite) { this.collectionTestsCollection = testSuite.firebase.native.firestore().collection('collection-tests'); this.documentTestsCollection = testSuite.firebase.native.firestore().collection('document-tests'); this.firestoreTestsCollection = testSuite.firebase.native.firestore().collection('firestore-tests'); - // Clean the collections in case the last run failed + // Make sure the collections are cleaned and initialised correctly await cleanCollection(this.collectionTestsCollection); await cleanCollection(this.documentTestsCollection); await cleanCollection(this.firestoreTestsCollection); - await this.collectionTestsCollection.add({ - baz: true, - daz: 123, - foo: 'bar', - gaz: 12.1234567, - naz: null, - }); + const tasks = []; + tasks.push(this.collectionTestsCollection.doc('col1').set(COL_1)); + tasks.push(this.documentTestsCollection.doc('doc1').set(DOC_1)); + tasks.push(this.documentTestsCollection.doc('doc2').set(DOC_2)); - await this.documentTestsCollection.doc('doc1').set({ - name: 'doc1', - }); + await Promise.all(tasks); }); testSuite.afterEach(async () => { - await cleanCollection(this.collectionTestsCollection); - await cleanCollection(this.documentTestsCollection); - await cleanCollection(this.firestoreTestsCollection); + // All data will be cleaned an re-initialised before each test + // Adding a clean here slows down the test suite dramatically }); testGroups.forEach((testGroup) => {