From 76d77da2e597d19fdf9173d7abe68f0048c432b1 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Tue, 10 Oct 2017 15:36:08 +0100 Subject: [PATCH] [firestore][android] Finish type mapping work for Android --- .../firestore/FirestoreSerialize.java | 105 ++++++++-------- .../firestore/RNFirebaseFirestore.java | 2 +- lib/modules/firestore/DocumentReference.js | 64 +--------- lib/modules/firestore/DocumentSnapshot.js | 54 +------- lib/modules/firestore/WriteBatch.js | 8 +- lib/modules/firestore/utils/serialize.js | 117 ++++++++++++++++++ .../tests/firestore/documentReferenceTests.js | 48 +++++-- 7 files changed, 223 insertions(+), 175 deletions(-) create mode 100644 lib/modules/firestore/utils/serialize.js 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 f97cb058..c7925bbf 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -6,6 +6,7 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.google.firebase.firestore.DocumentChange; @@ -15,12 +16,20 @@ import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.QuerySnapshot; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; +import io.invertase.firebase.Utils; + public class FirestoreSerialize { private static final String TAG = "FirestoreSerialize"; - private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); private static final String KEY_CHANGES = "changes"; private static final String KEY_DATA = "data"; private static final String KEY_DOC_CHANGE_DOCUMENT = "document"; @@ -143,38 +152,12 @@ public class FirestoreSerialize { * @param array Object[] * @return WritableArray */ - static WritableArray objectArrayToWritable(Object[] array) { + private static WritableArray objectArrayToWritable(Object[] array) { WritableArray writableArray = Arguments.createArray(); for (Object item : array) { - if (item == null) { - writableArray.pushNull(); - continue; - } - - Class itemClass = item.getClass(); - - if (itemClass == Boolean.class) { - writableArray.pushBoolean((Boolean) item); - } else if (itemClass == Integer.class) { - writableArray.pushDouble(((Integer) item).doubleValue()); - } else if (itemClass == Long.class) { - writableArray.pushDouble(((Long) item).doubleValue()); - } else if (itemClass == Double.class) { - writableArray.pushDouble((Double) item); - } else if (itemClass == Float.class) { - writableArray.pushDouble(((Float) item).doubleValue()); - } else if (itemClass == String.class) { - writableArray.pushString(item.toString()); - } else if (Map.class.isAssignableFrom(itemClass)) { - writableArray.pushMap((objectMapToWritable((Map) item))); - } else if (List.class.isAssignableFrom(itemClass)) { - List list = (List) item; - Object[] listAsArray = list.toArray(new Object[list.size()]); - writableArray.pushArray(objectArrayToWritable(listAsArray)); - } else { - throw new RuntimeException("Cannot convert object of type " + itemClass); - } + WritableMap typeMap = buildTypeMap(item); + writableArray.pushMap(typeMap); } return writableArray; @@ -191,44 +174,47 @@ public class FirestoreSerialize { typeMap.putString("type", "null"); typeMap.putNull("value"); } else { - Class valueClass = value.getClass(); - - if (valueClass == Boolean.class) { + if (value instanceof Boolean) { typeMap.putString("type", "boolean"); typeMap.putBoolean("value", (Boolean) value); - } else if (valueClass == Integer.class) { - map.putDouble(key, ((Integer) value).doubleValue()); - } else if (valueClass == Long.class) { - map.putDouble(key, ((Long) value).doubleValue()); - } else if (valueClass == Double.class) { + } else if (value instanceof Integer) { + typeMap.putString("type", "number"); + typeMap.putDouble("value", ((Integer) value).doubleValue()); + } else if (value instanceof Long) { + typeMap.putString("type", "number"); + typeMap.putDouble("value", ((Long) value).doubleValue()); + } else if (value instanceof Double) { typeMap.putString("type", "number"); typeMap.putDouble("value", (Double) value); - } else if (valueClass == Float.class) { + } else if (value instanceof Float) { typeMap.putString("type", "number"); typeMap.putDouble("value", ((Float) value).doubleValue()); - } else if (valueClass == String.class) { - map.putString(key, value.toString()); - } else if (Map.class.isAssignableFrom(valueClass)) { - map.putMap(key, (objectMapToWritable((Map) value))); - } else if (List.class.isAssignableFrom(valueClass)) { + } else if (value instanceof String) { + typeMap.putString("type", "string"); + typeMap.putString("value", (String) value); + } else if (Map.class.isAssignableFrom(value.getClass())) { + typeMap.putString("type", "object"); + typeMap.putMap("value", objectMapToWritable((Map) value)); + } else if (List.class.isAssignableFrom(value.getClass())) { + typeMap.putString("type", "array"); List list = (List) value; Object[] array = list.toArray(new Object[list.size()]); typeMap.putArray("value", objectArrayToWritable(array)); - } else if (valueClass == DocumentReference.class) { + } else if (value instanceof DocumentReference) { typeMap.putString("type", "reference"); typeMap.putString("value", ((DocumentReference) value).getPath()); - } else if (valueClass == GeoPoint.class) { + } else if (value instanceof GeoPoint) { typeMap.putString("type", "geopoint"); WritableMap geoPoint = Arguments.createMap(); geoPoint.putDouble("latitude", ((GeoPoint) value).getLatitude()); geoPoint.putDouble("longitude", ((GeoPoint) value).getLongitude()); typeMap.putMap("value", geoPoint); - } else if (valueClass == Date.class) { + } else if (value instanceof Date) { typeMap.putString("type", "date"); typeMap.putString("value", DATE_FORMAT.format((Date) value)); } else { // TODO: Changed to log an error rather than crash - is this correct? - Log.e(TAG, "buildTypeMap", new RuntimeException("Cannot convert object of type " + valueClass)); + Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass()); typeMap.putString("type", "null"); typeMap.putNull("value"); } @@ -249,7 +235,7 @@ public class FirestoreSerialize { return map; } - static List parseReadableArray(FirebaseFirestore firestore, ReadableArray readableArray) { + private static List parseReadableArray(FirebaseFirestore firestore, ReadableArray readableArray) { List list = new ArrayList<>(); if (readableArray != null) { for (int i = 0; i < readableArray.size(); i++) { @@ -259,7 +245,7 @@ public class FirestoreSerialize { return list; } - static Object parseTypeMap(FirebaseFirestore firestore, ReadableMap typeMap) { + private static Object parseTypeMap(FirebaseFirestore firestore, ReadableMap typeMap) { String type = typeMap.getString("type"); if ("boolean".equals(type)) { return typeMap.getBoolean("value"); @@ -292,4 +278,23 @@ public class FirestoreSerialize { return null; } } + + public static List parseDocumentBatches(FirebaseFirestore firestore, ReadableArray readableArray) { + List writes = new ArrayList<>(readableArray.size()); + for (int i = 0; i < readableArray.size(); i++) { + Map write = new HashMap<>(); + ReadableMap map = readableArray.getMap(i); + if (map.hasKey("data")) { + write.put("data", parseReadableMap(firestore, map.getMap("data"))); + } + if (map.hasKey("options")) { + write.put("options", Utils.recursivelyDeconstructReadableMap(map.getMap("options"))); + } + write.put("path", map.getString("path")); + write.put("type", map.getString("type")); + + writes.add(write); + } + return writes; + } } diff --git a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java index a55a4a6b..cf01f262 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java +++ b/android/src/main/java/io/invertase/firebase/firestore/RNFirebaseFirestore.java @@ -69,7 +69,7 @@ public class RNFirebaseFirestore extends ReactContextBaseJavaModule { final Promise promise) { FirebaseFirestore firestore = getFirestoreForApp(appName); WriteBatch batch = firestore.batch(); - final List writesArray = Utils.recursivelyDeconstructReadableArray(writes); + final List writesArray = FirestoreSerialize.parseDocumentBatches(firestore, writes); for (Object w : writesArray) { Map write = (Map) w; diff --git a/lib/modules/firestore/DocumentReference.js b/lib/modules/firestore/DocumentReference.js index 92aa3cee..71c1a43c 100644 --- a/lib/modules/firestore/DocumentReference.js +++ b/lib/modules/firestore/DocumentReference.js @@ -4,9 +4,9 @@ */ import CollectionReference from './CollectionReference'; import DocumentSnapshot from './DocumentSnapshot'; -import GeoPoint from './GeoPoint'; import Path from './Path'; -import { firestoreAutoId, isFunction, isObject, isString, typeOf } from '../../utils'; +import { buildNativeMap } from './utils/serialize'; +import { firestoreAutoId, isFunction, isObject, isString } from '../../utils'; export type WriteOptions = { merge?: boolean, @@ -153,7 +153,7 @@ export default class DocumentReference { } set(data: Object, writeOptions?: WriteOptions): Promise { - const nativeData = this._buildNativeMap(data); + const nativeData = buildNativeMap(data); return this._firestore._native .documentSet(this.path, nativeData, writeOptions); } @@ -177,8 +177,9 @@ export default class DocumentReference { data[key] = value; } } + const nativeData = buildNativeMap(data); return this._firestore._native - .documentUpdate(this.path, data); + .documentUpdate(this.path, nativeData); } /** @@ -196,59 +197,4 @@ export default class DocumentReference { this._firestore._native .documentOffSnapshot(this.path, listenerId); } - - _buildNativeMap(data: Object): Object { - const nativeData = {}; - if (data) { - Object.keys(data).forEach((key) => { - nativeData[key] = this._buildTypeMap(data[key]); - }); - } - return nativeData; - } - - _buildNativeArray(array: Object[]): any[] { - const nativeArray = []; - if (array) { - array.forEach((value) => { - nativeArray.push(this._buildTypeMap(value)); - }); - } - return nativeArray; - } - - _buildTypeMap(value: any): any { - const typeMap = {}; - const type = typeOf(value); - if (value === null) { - typeMap.type = 'null'; - typeMap.value = null; - } else if (type === 'boolean' || type === 'number' || type === 'string') { - typeMap.type = type; - typeMap.value = value; - } else if (type === 'array') { - typeMap.type = type; - typeMap.value = this._buildNativeArray(value); - } else if (type === 'object') { - if (value instanceof DocumentReference) { - typeMap.type = 'reference'; - typeMap.value = value.path; - } else if (value instanceof GeoPoint) { - typeMap.type = 'geopoint'; - typeMap.value = { - latititude: value.latitude, - longitude: value.longitude, - }; - } else if (value instanceof Date) { - typeMap.type = 'date'; - typeMap.value = value.toISOString(); - } else { - typeMap.type = 'object'; - typeMap.value = this._buildNativeMap(value); - } - } else { - console.warn(`Unknown data type received ${type}`); - } - return typeMap; - } } diff --git a/lib/modules/firestore/DocumentSnapshot.js b/lib/modules/firestore/DocumentSnapshot.js index 9e5f2cfa..ea8e4394 100644 --- a/lib/modules/firestore/DocumentSnapshot.js +++ b/lib/modules/firestore/DocumentSnapshot.js @@ -3,8 +3,8 @@ * DocumentSnapshot representation wrapper */ import DocumentReference from './DocumentReference'; -import GeoPoint from './GeoPoint'; import Path from './Path'; +import { parseNativeMap } from './utils/serialize'; export type SnapshotMetadata = { fromCache: boolean, @@ -17,23 +17,16 @@ export type DocumentSnapshotNativeData = { path: string, } -type TypeMap = { - type: 'array' | 'boolean' | 'geopoint' | 'null' | 'number' | 'object' | 'reference' | 'string', - value: any, -} - /** * @class DocumentSnapshot */ export default class DocumentSnapshot { _data: Object; - _firestore: Object; _metadata: SnapshotMetadata; _ref: DocumentReference; constructor(firestore: Object, nativeData: DocumentSnapshotNativeData) { - this._data = this._parseNativeMap(nativeData.data); - this._firestore = firestore; + this._data = parseNativeMap(firestore, nativeData.data); this._metadata = nativeData.metadata; this._ref = new DocumentReference(firestore, Path.fromName(nativeData.path)); } @@ -61,47 +54,4 @@ export default class DocumentSnapshot { get(fieldPath: string): any { return this._data[fieldPath]; } - - /** - * INTERNALS - */ - - _parseNativeMap(nativeData: Object): Object { - const data = {}; - if (nativeData) { - Object.keys(nativeData).forEach((key) => { - data[key] = this._parseTypeMap(nativeData[key]); - }); - } - return data; - } - - _parseNativeArray(nativeArray: Object[]): any[] { - const array = []; - if (nativeArray) { - nativeArray.forEach((typeMap) => { - array.push(this._parseTypeMap(typeMap)); - }); - } - return array; - } - - _parseTypeMap(typeMap: TypeMap): any { - const { type, value } = typeMap; - if (type === 'boolean' || type === 'number' || type === 'string' || type === 'null') { - return value; - } else if (type === 'array') { - return this._parseNativeArray(value); - } else if (type === 'object') { - return this._parseNativeMap(value); - } else if (type === 'reference') { - return new DocumentReference(this._firestore, Path.fromName(value)); - } else if (type === 'geopoint') { - return new GeoPoint(value.latitude, value.longitude); - } else if (type === 'date') { - return new Date(value); - } - console.warn(`Unknown data type received ${type}`); - return value; - } } diff --git a/lib/modules/firestore/WriteBatch.js b/lib/modules/firestore/WriteBatch.js index 0ed02094..8e657031 100644 --- a/lib/modules/firestore/WriteBatch.js +++ b/lib/modules/firestore/WriteBatch.js @@ -3,6 +3,7 @@ * WriteBatch representation wrapper */ import DocumentReference from './DocumentReference'; +import { buildNativeMap } from './utils/serialize'; import { isObject, isString } from '../../utils'; import type { WriteOptions } from './DocumentReference'; @@ -48,9 +49,9 @@ export default class WriteBatch { // validate.isDocumentReference('docRef', docRef); // validate.isDocument('data', data); // validate.isOptionalPrecondition('writeOptions', writeOptions); - + const nativeData = buildNativeMap(data); this._writes.push({ - data, + data: nativeData, options: writeOptions, path: docRef.path, type: 'SET', @@ -81,8 +82,9 @@ export default class WriteBatch { } } + const nativeData = buildNativeMap(data); this._writes.push({ - data, + data: nativeData, path: docRef.path, type: 'UPDATE', }); diff --git a/lib/modules/firestore/utils/serialize.js b/lib/modules/firestore/utils/serialize.js new file mode 100644 index 00000000..129165ba --- /dev/null +++ b/lib/modules/firestore/utils/serialize.js @@ -0,0 +1,117 @@ +// @flow + +import DocumentReference from '../DocumentReference'; +import GeoPoint from '../GeoPoint'; +import Path from '../Path'; +import { typeOf } from '../../../utils'; + +type TypeMap = { + type: 'array' | 'boolean' | 'geopoint' | 'null' | 'number' | 'object' | 'reference' | 'string', + value: any, +} + +/* + * Functions that build up the data needed to represent + * the different types available within Firestore + * for transmission to the native side + */ + +export const buildNativeMap = (data: Object): Object => { + const nativeData = {}; + if (data) { + Object.keys(data).forEach((key) => { + nativeData[key] = buildTypeMap(data[key]); + }); + } + return nativeData; +}; + +const buildNativeArray = (array: Object[]): any[] => { + const nativeArray = []; + if (array) { + array.forEach((value) => { + nativeArray.push(buildTypeMap(value)); + }); + } + return nativeArray; +}; + +const buildTypeMap = (value: any): any => { + const typeMap = {}; + const type = typeOf(value); + if (value === null) { + typeMap.type = 'null'; + typeMap.value = null; + } else if (type === 'boolean' || type === 'number' || type === 'string') { + typeMap.type = type; + typeMap.value = value; + } else if (type === 'array') { + typeMap.type = type; + typeMap.value = buildNativeArray(value); + } else if (type === 'object') { + if (value instanceof DocumentReference) { + typeMap.type = 'reference'; + typeMap.value = value.path; + } else if (value instanceof GeoPoint) { + typeMap.type = 'geopoint'; + typeMap.value = { + latitude: value.latitude, + longitude: value.longitude, + }; + } else if (value instanceof Date) { + typeMap.type = 'date'; + typeMap.value = value.toISOString(); + } else { + typeMap.type = 'object'; + typeMap.value = buildNativeMap(value); + } + } else { + console.warn(`Unknown data type received ${type}`); + } + return typeMap; +}; + +/* + * Functions that parse the received from the native + * side and converts to the correct Firestore JS types + */ + +export const parseNativeMap = (firestore: Object, nativeData: Object): Object => { + let data; + if (nativeData) { + data = {}; + Object.keys(nativeData).forEach((key) => { + data[key] = parseTypeMap(firestore, nativeData[key]); + }); + } + return data; +}; + +const parseNativeArray = (firestore: Object, nativeArray: Object[]): any[] => { + const array = []; + if (nativeArray) { + nativeArray.forEach((typeMap) => { + array.push(parseTypeMap(firestore, typeMap)); + }); + } + return array; +}; + +const parseTypeMap = (firestore: Object, typeMap: TypeMap): any => { + const { type, value } = typeMap; + if (type === 'boolean' || type === 'number' || type === 'string' || type === 'null') { + return value; + } else if (type === 'array') { + return parseNativeArray(firestore, value); + } else if (type === 'object') { + return parseNativeMap(firestore, value); + } else if (type === 'reference') { + return new DocumentReference(firestore, Path.fromName(value)); + } else if (type === 'geopoint') { + return new GeoPoint(value.latitude, value.longitude); + } else if (type === 'date') { + return new Date(value); + } + console.warn(`Unknown data type received ${type}`); + return value; +}; diff --git a/tests/src/tests/firestore/documentReferenceTests.js b/tests/src/tests/firestore/documentReferenceTests.js index fc6fe6e7..5232148a 100644 --- a/tests/src/tests/firestore/documentReferenceTests.js +++ b/tests/src/tests/firestore/documentReferenceTests.js @@ -2,6 +2,7 @@ import sinon from 'sinon'; import 'should-sinon'; import should from 'should'; + function documentReferenceTests({ describe, it, context, firebase }) { describe('DocumentReference', () => { context('class', () => { @@ -28,16 +29,6 @@ function documentReferenceTests({ describe, it, context, firebase }) { }); }); - context('get()', () => { - it('should return DocumentReference field', async () => { - const docRef = firebase.native.firestore().doc('users/6hyiyxQ00JzdWlKFyH3E'); - const doc = await docRef.get(); - console.log('Doc', doc); - should.equal(doc.exists, true); - await docRef.set(doc.data()); - }); - }); - context('onSnapshot()', () => { it('calls callback with the initial data and then when value changes', async () => { const docRef = firebase.native.firestore().doc('document-tests/doc1'); @@ -420,6 +411,43 @@ function documentReferenceTests({ describe, it, context, firebase }) { }); }); }); + + context('types', () => { + it('should handle Date field', async () => { + const docRef = firebase.native.firestore().doc('document-tests/reference'); + await docRef.set({ + field: new Date(), + }); + + const doc = await docRef.get(); + doc.data().field.should.be.instanceof(Date); + }); + }); + + context('types', () => { + it('should handle DocumentReference field', async () => { + const docRef = firebase.native.firestore().doc('document-tests/reference'); + await docRef.set({ + field: firebase.native.firestore().doc('test/field'), + }); + + const doc = await docRef.get(); + should.equal(doc.data().field.path, 'test/field'); + }); + }); + + context('types', () => { + it('should handle GeoPoint field', async () => { + const docRef = firebase.native.firestore().doc('document-tests/reference'); + await docRef.set({ + field: new firebase.native.firestore.GeoPoint(1.01, 1.02), + }); + + const doc = await docRef.get(); + should.equal(doc.data().field.latitude, 1.01); + should.equal(doc.data().field.longitude, 1.02); + }); + }); }); }