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 fad1fe3f..be2e24be 100644 --- a/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java +++ b/android/src/main/java/io/invertase/firebase/firestore/FirestoreSerialize.java @@ -18,6 +18,7 @@ import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.GeoPoint; import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.SnapshotMetadata; import java.util.ArrayList; import java.util.Date; @@ -25,19 +26,53 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; + import io.invertase.firebase.Utils; -public class FirestoreSerialize { +class FirestoreSerialize { private static final String TAG = "FirestoreSerialize"; - private static final String KEY_CHANGES = "changes"; + + // Keys + private static final String TYPE = "type"; + private static final String VALUE = "value"; private static final String KEY_DATA = "data"; + private static final String KEY_PATH = "path"; + private static final String KEY_META = "metadata"; + private static final String KEY_CHANGES = "changes"; + private static final String KEY_OPTIONS = "options"; + private static final String KEY_LATITUDE = "latitude"; + private static final String KEY_LONGITUDE = "longitude"; + private static final String KEY_DOCUMENTS = "documents"; + private static final String KEY_DOC_CHANGE_TYPE = "type"; + private static final String KEY_META_FROM_CACHE = "fromCache"; private static final String KEY_DOC_CHANGE_DOCUMENT = "document"; private static final String KEY_DOC_CHANGE_NEW_INDEX = "newIndex"; private static final String KEY_DOC_CHANGE_OLD_INDEX = "oldIndex"; - private static final String KEY_DOC_CHANGE_TYPE = "type"; - private static final String KEY_DOCUMENTS = "documents"; - private static final String KEY_METADATA = "metadata"; - private static final String KEY_PATH = "path"; + private static final String KEY_META_HAS_PENDING_WRITES = "hasPendingWrites"; + + // Types + private static final String TYPE_NAN = "nan"; + private static final String TYPE_NULL = "null"; + private static final String TYPE_BLOB = "blob"; + private static final String TYPE_DATE = "date"; + private static final String TYPE_ARRAY = "array"; + private static final String TYPE_STRING = "string"; + private static final String TYPE_NUMBER = "number"; + private static final String TYPE_OBJECT = "object"; + private static final String TYPE_BOOLEAN = "boolean"; + private static final String TYPE_GEOPOINT = "geopoint"; + private static final String TYPE_INFINITY = "infinity"; + private static final String TYPE_REFERENCE = "reference"; + private static final String TYPE_DOCUMENTID = "documentid"; + private static final String TYPE_FIELDVALUE = "fieldvalue"; + private static final String TYPE_FIELDVALUE_DELETE = "delete"; + private static final String TYPE_FIELDVALUE_TIMESTAMP = "timestamp"; + + // Document Change Types + private static final String CHANGE_ADDED = "added"; + private static final String CHANGE_MODIFIED = "modified"; + private static final String CHANGE_REMOVED = "removed"; /** * Convert a DocumentSnapshot instance into a React Native WritableMap @@ -46,67 +81,59 @@ public class FirestoreSerialize { * @return WritableMap */ static WritableMap snapshotToWritableMap(DocumentSnapshot documentSnapshot) { - WritableMap documentMap = Arguments.createMap(); - - documentMap.putString( - KEY_PATH, - documentSnapshot - .getReference() - .getPath() - ); - if (documentSnapshot.exists()) { - documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); - } - - // metadata WritableMap metadata = Arguments.createMap(); - metadata.putBoolean( - "fromCache", - documentSnapshot - .getMetadata() - .isFromCache() - ); - metadata.putBoolean( - "hasPendingWrites", - documentSnapshot - .getMetadata() - .hasPendingWrites() - ); - documentMap.putMap(KEY_METADATA, metadata); + WritableMap documentMap = Arguments.createMap(); + SnapshotMetadata snapshotMetadata = documentSnapshot.getMetadata(); + + // build metadata + metadata.putBoolean(KEY_META_FROM_CACHE, snapshotMetadata.isFromCache()); + metadata.putBoolean(KEY_META_HAS_PENDING_WRITES, snapshotMetadata.hasPendingWrites()); + + documentMap.putMap(KEY_META, metadata); + documentMap.putString(KEY_PATH, documentSnapshot.getReference().getPath()); + if (documentSnapshot.exists()) + documentMap.putMap(KEY_DATA, objectMapToWritable(documentSnapshot.getData())); + return documentMap; } + /** + * Convert a Firestore QuerySnapshot instance to a RN serializable WritableMap type map + * + * @param querySnapshot QuerySnapshot + * @return WritableMap + */ static WritableMap snapshotToWritableMap(QuerySnapshot querySnapshot) { - WritableMap queryMap = Arguments.createMap(); - - List documentChanges = querySnapshot.getDocumentChanges(); - queryMap.putArray(KEY_CHANGES, documentChangesToWritableArray(documentChanges)); - - // documents + WritableMap metadata = Arguments.createMap(); + WritableMap writableMap = Arguments.createMap(); WritableArray documents = Arguments.createArray(); + + SnapshotMetadata snapshotMetadata = querySnapshot.getMetadata(); List documentSnapshots = querySnapshot.getDocuments(); + List documentChanges = querySnapshot.getDocumentChanges(); + + // convert documents documents for (DocumentSnapshot documentSnapshot : documentSnapshots) { documents.pushMap(snapshotToWritableMap(documentSnapshot)); } - queryMap.putArray(KEY_DOCUMENTS, documents); - // metadata - WritableMap metadata = Arguments.createMap(); - metadata.putBoolean( - "fromCache", - querySnapshot - .getMetadata() - .isFromCache() - ); - metadata.putBoolean( - "hasPendingWrites", - querySnapshot - .getMetadata() - .hasPendingWrites() - ); - queryMap.putMap(KEY_METADATA, metadata); + // build metadata + metadata.putBoolean(KEY_META_FROM_CACHE, snapshotMetadata.isFromCache()); + metadata.putBoolean(KEY_META_HAS_PENDING_WRITES, snapshotMetadata.hasPendingWrites()); - return queryMap; + // set metadata + writableMap.putMap(KEY_META, metadata); + + // set documents + writableMap.putArray(KEY_DOCUMENTS, documents); + + // set document changes + writableMap.putArray( + KEY_CHANGES, + documentChangesToWritableArray(documentChanges) + ); + + return writableMap; } /** @@ -117,9 +144,11 @@ public class FirestoreSerialize { */ private static WritableArray documentChangesToWritableArray(List documentChanges) { WritableArray documentChangesWritable = Arguments.createArray(); + for (DocumentChange documentChange : documentChanges) { documentChangesWritable.pushMap(documentChangeToWritableMap(documentChange)); } + return documentChangesWritable; } @@ -134,19 +163,21 @@ public class FirestoreSerialize { switch (documentChange.getType()) { case ADDED: - documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "added"); - break; - case REMOVED: - documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "removed"); + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_ADDED); break; case MODIFIED: - documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, "modified"); + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_MODIFIED); + break; + case REMOVED: + documentChangeMap.putString(KEY_DOC_CHANGE_TYPE, CHANGE_REMOVED); + break; } documentChangeMap.putMap( KEY_DOC_CHANGE_DOCUMENT, snapshotToWritableMap(documentChange.getDocument()) ); + documentChangeMap.putInt(KEY_DOC_CHANGE_NEW_INDEX, documentChange.getNewIndex()); documentChangeMap.putInt(KEY_DOC_CHANGE_OLD_INDEX, documentChange.getOldIndex()); @@ -161,15 +192,17 @@ public class FirestoreSerialize { */ private static WritableMap objectMapToWritable(Map map) { WritableMap writableMap = Arguments.createMap(); + for (Map.Entry entry : map.entrySet()) { WritableMap typeMap = buildTypeMap(entry.getValue()); writableMap.putMap(entry.getKey(), typeMap); } + return writableMap; } /** - * Converts an Object array into a React Native WritableArray. + * Converts a Object array into a React Native WritableArray. * * @param array Object[] * @return WritableArray @@ -186,155 +219,285 @@ public class FirestoreSerialize { } /** - * Detects an objects type and creates a Map to represent the type and value. + * Convert an Object to a type map for use in JS land to convert to JS equivalent. * * @param value Object + * @return WritableMap */ private static WritableMap buildTypeMap(Object value) { WritableMap typeMap = Arguments.createMap(); + if (value == null) { - typeMap.putString("type", "null"); - typeMap.putNull("value"); - } else { - if (value instanceof Boolean) { - typeMap.putString("type", "boolean"); - typeMap.putBoolean("value", (Boolean) value); - } 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 (value instanceof Float) { - typeMap.putString("type", "number"); - typeMap.putDouble("value", ((Float) value).doubleValue()); - } 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 (value instanceof DocumentReference) { - typeMap.putString("type", "reference"); - typeMap.putString("value", ((DocumentReference) value).getPath()); - } 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 (value instanceof Date) { - typeMap.putString("type", "date"); - typeMap.putDouble("value", ((Date) value).getTime()); - } else if (value instanceof Blob) { - typeMap.putString("type", "blob"); - typeMap.putString("value", Base64.encodeToString(((Blob) value).toBytes(), Base64.NO_WRAP)); - } else { - Log.e(TAG, "buildTypeMap: Cannot convert object of type " + value.getClass()); - typeMap.putString("type", "null"); - typeMap.putNull("value"); - } + typeMap.putString(TYPE, TYPE_NULL); + typeMap.putNull(VALUE); + return typeMap; } + if (value instanceof Boolean) { + typeMap.putString(TYPE, TYPE_BOOLEAN); + typeMap.putBoolean(VALUE, (Boolean) value); + return typeMap; + } + + if (value instanceof Integer) { + typeMap.putString(TYPE, TYPE_NUMBER); + typeMap.putDouble(VALUE, ((Integer) value).doubleValue()); + return typeMap; + } + + if (value instanceof Double) { + Double doubleValue = (Double) value; + + if (Double.isInfinite(doubleValue)) { + typeMap.putString(TYPE, TYPE_INFINITY); + return typeMap; + } + + if (Double.isNaN(doubleValue)) { + typeMap.putString(TYPE, TYPE_NAN); + return typeMap; + } + + typeMap.putString(TYPE, TYPE_NUMBER); + typeMap.putDouble(VALUE, doubleValue); + return typeMap; + } + + if (value instanceof Float) { + typeMap.putString(TYPE, TYPE_NUMBER); + typeMap.putDouble(VALUE, ((Float) value).doubleValue()); + return typeMap; + } + + if (value instanceof Long) { + typeMap.putString(TYPE, TYPE_NUMBER); + typeMap.putDouble(VALUE, ((Long) value).doubleValue()); + return typeMap; + } + + if (value instanceof String) { + typeMap.putString(TYPE, TYPE_STRING); + typeMap.putString(VALUE, (String) value); + return typeMap; + } + + if (value instanceof Date) { + typeMap.putString(TYPE, TYPE_DATE); + typeMap.putDouble(VALUE, ((Date) value).getTime()); + return typeMap; + } + + if (Map.class.isAssignableFrom(value.getClass())) { + typeMap.putString(TYPE, TYPE_OBJECT); + typeMap.putMap(VALUE, objectMapToWritable((Map) value)); + return typeMap; + } + + if (List.class.isAssignableFrom(value.getClass())) { + typeMap.putString(TYPE, TYPE_ARRAY); + List list = (List) value; + Object[] array = list.toArray(new Object[list.size()]); + typeMap.putArray(VALUE, objectArrayToWritable(array)); + return typeMap; + } + + if (value instanceof DocumentReference) { + typeMap.putString(TYPE, TYPE_REFERENCE); + typeMap.putString(VALUE, ((DocumentReference) value).getPath()); + return typeMap; + } + + if (value instanceof GeoPoint) { + WritableMap geoPoint = Arguments.createMap(); + + geoPoint.putDouble(KEY_LATITUDE, ((GeoPoint) value).getLatitude()); + geoPoint.putDouble(KEY_LONGITUDE, ((GeoPoint) value).getLongitude()); + + typeMap.putMap(VALUE, geoPoint); + typeMap.putString(TYPE, TYPE_GEOPOINT); + + return typeMap; + } + + if (value instanceof Blob) { + typeMap.putString(TYPE, TYPE_BLOB); + typeMap.putString(VALUE, Base64.encodeToString(((Blob) value).toBytes(), Base64.NO_WRAP)); + return typeMap; + } + + Log.w(TAG, "Unknown object of type " + value.getClass()); + typeMap.putString(TYPE, TYPE_NULL); + typeMap.putNull(VALUE); return typeMap; } + /** + * Converts a ReadableMap to a usable format for Firestore + * + * @param firestore FirebaseFirestore + * @param readableMap ReadableMap + * @return Map<> + */ static Map parseReadableMap( FirebaseFirestore firestore, - ReadableMap readableMap + @Nullable ReadableMap readableMap ) { Map map = new HashMap<>(); - if (readableMap != null) { - ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); - while (iterator.hasNextKey()) { - String key = iterator.nextKey(); - map.put(key, parseTypeMap(firestore, readableMap.getMap(key))); - } + if (readableMap == null) return map; + + ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + map.put(key, parseTypeMap(firestore, readableMap.getMap(key))); } + return map; } - static List parseReadableArray(FirebaseFirestore firestore, ReadableArray readableArray) { + /** + * Convert a RN array to a valid Firestore array + * + * @param firestore FirebaseFirestore + * @param readableArray ReadableArray + * @return List + */ + static List parseReadableArray( + FirebaseFirestore firestore, + @Nullable ReadableArray readableArray + ) { List list = new ArrayList<>(); - if (readableArray != null) { - for (int i = 0; i < readableArray.size(); i++) { - list.add(parseTypeMap(firestore, readableArray.getMap(i))); - } + if (readableArray == null) return list; + + for (int i = 0; i < readableArray.size(); i++) { + list.add(parseTypeMap(firestore, readableArray.getMap(i))); } + return list; } + /** + * Convert a JS type to a Firestore type + * + * @param firestore FirebaseFirestore + * @param typeMap ReadableMap + * @return Object + */ static Object parseTypeMap(FirebaseFirestore firestore, ReadableMap typeMap) { - String type = typeMap.getString("type"); - if ("boolean".equals(type)) { - return typeMap.getBoolean("value"); - } else if ("number".equals(type)) { - return typeMap.getDouble("value"); - } else if ("string".equals(type)) { - return typeMap.getString("value"); - } else if ("null".equals(type)) { - return null; - } else if ("array".equals(type)) { - return parseReadableArray(firestore, typeMap.getArray("value")); - } else if ("object".equals(type)) { - return parseReadableMap(firestore, typeMap.getMap("value")); - } else if ("reference".equals(type)) { - String path = typeMap.getString("value"); - return firestore.document(path); - } else if ("geopoint".equals(type)) { - ReadableMap geoPoint = typeMap.getMap("value"); - return new GeoPoint(geoPoint.getDouble("latitude"), geoPoint.getDouble("longitude")); - } else if ("blob".equals(type)) { - String base64String = typeMap.getString("value"); - return Blob.fromBytes(Base64.decode(base64String, Base64.NO_WRAP)); - } 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)) { - 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); + String type = typeMap.getString(TYPE); + + if (TYPE_NULL.equals(type)) { return null; } + + if (TYPE_BOOLEAN.equals(type)) { + return typeMap.getBoolean(VALUE); + } + + if (TYPE_NAN.equals(type)) { + return Double.NaN; + } + + if (TYPE_NUMBER.equals(type)) { + return typeMap.getDouble(VALUE); + } + + if (TYPE_INFINITY.equals(type)) { + return Double.POSITIVE_INFINITY; + } + + if (TYPE_STRING.equals(type)) { + return typeMap.getString(VALUE); + } + + if (TYPE_ARRAY.equals(type)) { + return parseReadableArray(firestore, typeMap.getArray(VALUE)); + } + + if (TYPE_OBJECT.equals(type)) { + return parseReadableMap(firestore, typeMap.getMap(VALUE)); + } + + if (TYPE_DATE.equals(type)) { + Double time = typeMap.getDouble(VALUE); + return new Date(time.longValue()); + } + + /* -------------------------- + * Firestore Specific Types + * -------------------------- */ + + if (TYPE_DOCUMENTID.equals(type)) { + return FieldPath.documentId(); + } + + if (TYPE_GEOPOINT.equals(type)) { + ReadableMap geoPoint = typeMap.getMap(VALUE); + return new GeoPoint(geoPoint.getDouble(KEY_LATITUDE), geoPoint.getDouble(KEY_LONGITUDE)); + } + + if (TYPE_BLOB.equals(type)) { + String base64String = typeMap.getString(VALUE); + return Blob.fromBytes(Base64.decode(base64String, Base64.NO_WRAP)); + } + + if (TYPE_REFERENCE.equals(type)) { + String path = typeMap.getString(VALUE); + return firestore.document(path); + } + + if (TYPE_FIELDVALUE.equals(type)) { + String fieldValueType = typeMap.getString(VALUE); + + if (TYPE_FIELDVALUE_TIMESTAMP.equals(fieldValueType)) { + return FieldValue.serverTimestamp(); + } + + if (TYPE_FIELDVALUE_DELETE.equals(fieldValueType)) { + return FieldValue.delete(); + } + + Log.w(TAG, "Unknown FieldValue type: " + fieldValueType); + return null; + } + + Log.w(TAG, "Unknown object of type " + type); + return null; } + /** + * Parse JS batches array + * + * @param firestore FirebaseFirestore + * @param readableArray ReadableArray + * @return List + */ 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"))); + + write.put(TYPE, map.getString(TYPE)); + write.put(KEY_PATH, map.getString(KEY_PATH)); + + if (map.hasKey(KEY_DATA)) { + write.put(KEY_DATA, parseReadableMap(firestore, map.getMap(KEY_DATA))); } - if (map.hasKey("options")) { - write.put("options", Utils.recursivelyDeconstructReadableMap(map.getMap("options"))); + + if (map.hasKey(KEY_OPTIONS)) { + write.put( + KEY_OPTIONS, + Utils.recursivelyDeconstructReadableMap(map.getMap(KEY_OPTIONS)) + ); } - write.put("path", map.getString("path")); - write.put("type", map.getString("type")); writes.add(write); } + return writes; } } diff --git a/tests/e2e/firestore/collection/snapshot.e2e.js b/tests/e2e/firestore/collection/snapshot.e2e.js index e6aba794..bd6114ce 100644 --- a/tests/e2e/firestore/collection/snapshot.e2e.js +++ b/tests/e2e/firestore/collection/snapshot.e2e.js @@ -103,7 +103,6 @@ describe('firestore()', () => { 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);