From 571d9b6031fe4236b2c6b4b96174419fec735939 Mon Sep 17 00:00:00 2001 From: Salakar Date: Sun, 23 Sep 2018 07:49:50 +0100 Subject: [PATCH] [firestore][ios] re-write FirestoreDocReference internals + add support for Infinity & NaN numbers --- .../RNFirebaseFirestoreDocumentReference.h | 1 - .../RNFirebaseFirestoreDocumentReference.m | 636 +++++++++++------- src/modules/firestore/firestoreTypes.flow.js | 2 + src/modules/firestore/utils/serialize.js | 44 +- tests/helpers/firestore.js | 2 + 5 files changed, 437 insertions(+), 248 deletions(-) diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h index ecd83e92..7f4cbc60 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.h @@ -24,7 +24,6 @@ - (void)onSnapshot:(NSString *)listenerId docListenOptions:(NSDictionary *) docListenOptions; - (void)set:(NSDictionary *)data options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; - (void)update:(NSDictionary *)data resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject; -- (BOOL)hasListeners; + (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot; + (NSDictionary *)parseJSMap:(FIRFirestore *) firestore jsMap:(NSDictionary *) jsMap; + (NSArray *)parseJSArray:(FIRFirestore *) firestore jsArray:(NSArray *) jsArray; diff --git a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m index bca7d498..7e6457b0 100644 --- a/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m +++ b/ios/RNFirebase/firestore/RNFirebaseFirestoreDocumentReference.m @@ -5,295 +5,439 @@ #if __has_include() static NSMutableDictionary *_listeners; +static NSString *const typeKey = @"type"; +static NSString *const keyPath = @"path"; +static NSString *const keyData = @"data"; +static NSString *const keyError = @"error"; +static NSString *const valueKey = @"value"; +static NSString *const keyMerge = @"merge"; +static NSString *const keyAppName = @"appName"; +static NSString *const keyLatitude = @"latitude"; +static NSString *const keyMetadata = @"metadata"; +static NSString *const keyLongitude = @"longitude"; +static NSString *const keyFromCache = @"fromCache"; +static NSString *const keyListenerId = @"listenerId"; +static NSString *const keyDocumentSnapshot = @"documentSnapshot"; +static NSString *const keyHasPendingWrites = @"hasPendingWrites"; +static NSString *const keyIncludeMetaChanges = @"includeMetadataChanges"; + +static NSString *const typeNaN = @"nan"; +static NSString *const typeNull = @"null"; +static NSString *const typeBlob = @"blob"; +static NSString *const typeDate = @"date"; +static NSString *const typeArray = @"array"; +static NSString *const typeObject = @"object"; +static NSString *const typeString = @"string"; +static NSString *const typeNumber = @"number"; +static NSString *const typeDelete = @"delete"; +static NSString *const typeBoolean = @"boolean"; +static NSString *const typeInfinity = @"infinity"; +static NSString *const typeGeoPoint = @"geopoint"; +static NSString *const typeTimestamp = @"timestamp"; +static NSString *const typeReference = @"reference"; +static NSString *const typeDocumentId = @"documentid"; +static NSString *const typeFieldValue = @"fieldvalue"; - (id)initWithPath:(RCTEventEmitter *)emitter - appDisplayName:(NSString *) appDisplayName - path:(NSString *) path { - self = [super init]; - if (self) { - _emitter = emitter; - _appDisplayName = appDisplayName; - _path = path; - _ref = [[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] documentWithPath:_path]; - } - // Initialise the static listeners object if required - if (!_listeners) { - _listeners = [[NSMutableDictionary alloc] init]; - } - return self; + appDisplayName:(NSString *)appDisplayName + path:(NSString *)path { + self = [super init]; + + if (self) { + _emitter = emitter; + _appDisplayName = appDisplayName; + _path = path; + _ref = [[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] documentWithPath:_path]; + } + + // Initialise the static listeners object if required + if (!_listeners) { + _listeners = [[NSMutableDictionary alloc] init]; + } + + return self; } -- (void)delete:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject { - [_ref deleteDocumentWithCompletion:^(NSError * _Nullable error) { - [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; - }]; +- (void)delete:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + [_ref deleteDocumentWithCompletion:^(NSError *_Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; } -- (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; } - [_ref getDocumentWithSource:source completion:^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) { - if (error) { - [RNFirebaseFirestore promiseRejectException:reject error:error]; - } else { - NSDictionary *data = [RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]; - resolve(data); - } - }]; -} - -+ (void)offSnapshot:(NSString *) listenerId { - id listener = _listeners[listenerId]; - if (listener) { - [_listeners removeObjectForKey:listenerId]; - [listener remove]; - } -} - -- (void)onSnapshot:(NSString *) listenerId - docListenOptions:(NSDictionary *) docListenOptions { - if (_listeners[listenerId] == nil) { - id listenerBlock = ^(FIRDocumentSnapshot * _Nullable snapshot, NSError * _Nullable error) { - if (error) { - id listener = _listeners[listenerId]; - if (listener) { - [_listeners removeObjectForKey:listenerId]; - [listener remove]; - } - [self handleDocumentSnapshotError:listenerId error:error]; - } else { - [self handleDocumentSnapshotEvent:listenerId documentSnapshot:snapshot]; - } - }; - bool includeMetadataChanges; - if (docListenOptions && docListenOptions[@"includeMetadataChanges"]) { - includeMetadataChanges = true; - } else { - includeMetadataChanges = false; - } - id listener = [_ref addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges listener:listenerBlock]; - _listeners[listenerId] = listener; - } -} - -- (void)set:(NSDictionary *) data - options:(NSDictionary *) options - resolver:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject { - NSDictionary *dictionary = [RNFirebaseFirestoreDocumentReference parseJSMap:[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] jsMap:data]; - if (options && options[@"merge"]) { - [_ref setData:dictionary merge:true completion:^(NSError * _Nullable error) { - [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; - }]; - } else { - [_ref setData:dictionary completion:^(NSError * _Nullable error) { - [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; - }]; - } -} - -- (void)update:(NSDictionary *) data - resolver:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject { - NSDictionary *dictionary = [RNFirebaseFirestoreDocumentReference parseJSMap:[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] jsMap:data]; - [_ref updateData:dictionary completion:^(NSError * _Nullable error) { - [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; - }]; -} - -- (BOOL)hasListeners { - return [[_listeners allKeys] count] > 0; -} - -+ (void)handleWriteResponse:(NSError *) error - resolver:(RCTPromiseResolveBlock) resolve - rejecter:(RCTPromiseRejectBlock) reject { + } else { + source = FIRFirestoreSourceDefault; + } + [_ref getDocumentWithSource:source completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error) { if (error) { - [RNFirebaseFirestore promiseRejectException:reject error:error]; + [RNFirebaseFirestore promiseRejectException:reject error:error]; } else { - resolve(nil); + NSDictionary *data = [RNFirebaseFirestoreDocumentReference snapshotToDictionary:snapshot]; + resolve(data); } + }]; +} + ++ (void)offSnapshot:(NSString *)listenerId { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } +} + +- (void)onSnapshot:(NSString *)listenerId + docListenOptions:(NSDictionary *)docListenOptions { + if (_listeners[listenerId] == nil) { + id listenerBlock = ^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error) { + if (error) { + id listener = _listeners[listenerId]; + if (listener) { + [_listeners removeObjectForKey:listenerId]; + [listener remove]; + } + [self handleDocumentSnapshotError:listenerId error:error]; + } else { + [self handleDocumentSnapshotEvent:listenerId documentSnapshot:snapshot]; + } + }; + bool includeMetadataChanges; + if (docListenOptions && docListenOptions[keyIncludeMetaChanges]) { + includeMetadataChanges = true; + } else { + includeMetadataChanges = false; + } + id + listener = [_ref addSnapshotListenerWithIncludeMetadataChanges:includeMetadataChanges listener:listenerBlock]; + _listeners[listenerId] = listener; + } +} + +- (void)set:(NSDictionary *)data + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + NSDictionary *dictionary = + [RNFirebaseFirestoreDocumentReference parseJSMap:[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] jsMap:data]; + if (options && options[keyMerge]) { + [_ref setData:dictionary merge:true completion:^(NSError *_Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; + } else { + [_ref setData:dictionary completion:^(NSError *_Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; + } +} + +- (void)update:(NSDictionary *)data + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + NSDictionary *dictionary = + [RNFirebaseFirestoreDocumentReference parseJSMap:[RNFirebaseFirestore getFirestoreForApp:_appDisplayName] jsMap:data]; + [_ref updateData:dictionary completion:^(NSError *_Nullable error) { + [RNFirebaseFirestoreDocumentReference handleWriteResponse:error resolver:resolve rejecter:reject]; + }]; +} + ++ (void)handleWriteResponse:(NSError *)error + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + if (error) { + [RNFirebaseFirestore promiseRejectException:reject error:error]; + } else { + resolve(nil); + } } + (NSDictionary *)snapshotToDictionary:(FIRDocumentSnapshot *)documentSnapshot { - NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; - [snapshot setValue:documentSnapshot.reference.path forKey:@"path"]; - if (documentSnapshot.exists) { - [snapshot setValue:[RNFirebaseFirestoreDocumentReference buildNativeMap:documentSnapshot.data] forKey:@"data"]; - } - if (documentSnapshot.metadata) { - NSMutableDictionary *metadata = [[NSMutableDictionary alloc] init]; - [metadata setValue:@(documentSnapshot.metadata.fromCache) forKey:@"fromCache"]; - [metadata setValue:@(documentSnapshot.metadata.hasPendingWrites) forKey:@"hasPendingWrites"]; - [snapshot setValue:metadata forKey:@"metadata"]; - } - return snapshot; + NSMutableDictionary *snapshot = [[NSMutableDictionary alloc] init]; + [snapshot setValue:documentSnapshot.reference.path forKey:keyPath]; + if (documentSnapshot.exists) { + [snapshot setValue:[RNFirebaseFirestoreDocumentReference buildNativeMap:documentSnapshot.data] forKey:keyData]; + } + if (documentSnapshot.metadata) { + NSMutableDictionary *metadata = [[NSMutableDictionary alloc] init]; + [metadata setValue:@(documentSnapshot.metadata.fromCache) forKey:keyFromCache]; + [metadata setValue:@(documentSnapshot.metadata.hasPendingWrites) forKey:keyHasPendingWrites]; + [snapshot setValue:metadata forKey:keyMetadata]; + } + return snapshot; } - (void)handleDocumentSnapshotError:(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:_path forKey:keyPath]; + [event setValue:listenerId forKey:keyListenerId]; + [event setValue:_appDisplayName forKey:keyAppName]; + [event setValue:[RNFirebaseFirestore getJSError:error] forKey:keyError]; - [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } - (void)handleDocumentSnapshotEvent:(NSString *)listenerId documentSnapshot:(FIRDocumentSnapshot *)documentSnapshot { - NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; - [event setValue:_appDisplayName forKey:@"appName"]; - [event setValue:_path forKey:@"path"]; - [event setValue:listenerId forKey:@"listenerId"]; - [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:@"documentSnapshot"]; + NSMutableDictionary *event = [[NSMutableDictionary alloc] init]; + [event setValue:_path forKey:keyPath]; + [event setValue:listenerId forKey:keyListenerId]; + [event setValue:_appDisplayName forKey:keyAppName]; + [event setValue:[RNFirebaseFirestoreDocumentReference snapshotToDictionary:documentSnapshot] forKey:keyDocumentSnapshot]; - [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; + [RNFirebaseUtil sendJSEvent:self.emitter name:FIRESTORE_DOCUMENT_SYNC_EVENT body:event]; } - + (NSDictionary *)buildNativeMap:(NSDictionary *)nativeMap { - NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; - [nativeMap enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { - NSDictionary *typeMap = [RNFirebaseFirestoreDocumentReference buildTypeMap:obj]; - map[key] = typeMap; - }]; + NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; + [nativeMap enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) { + NSDictionary *typeMap = [RNFirebaseFirestoreDocumentReference buildTypeMap:obj]; + map[key] = typeMap; + }]; - return map; + return map; } + (NSArray *)buildNativeArray:(NSArray *)nativeArray { - NSMutableArray *array = [[NSMutableArray alloc] init]; - [nativeArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSDictionary *typeMap = [RNFirebaseFirestoreDocumentReference buildTypeMap:obj]; - [array addObject:typeMap]; - }]; + NSMutableArray *array = [[NSMutableArray alloc] init]; + [nativeArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + NSDictionary *typeMap = [RNFirebaseFirestoreDocumentReference buildTypeMap:obj]; + [array addObject:typeMap]; + }]; - return array; + return array; } -+ (NSDictionary *)buildTypeMap:(id) value { - NSMutableDictionary *typeMap = [[NSMutableDictionary alloc] init]; - if (!value) { - typeMap[@"type"] = @"null"; - } else if ([value isKindOfClass:[NSString class]]) { - typeMap[@"type"] = @"string"; - typeMap[@"value"] = value; - } else if ([value isKindOfClass:[NSDictionary class]]) { - typeMap[@"type"] = @"object"; - typeMap[@"value"] = [RNFirebaseFirestoreDocumentReference buildNativeMap:value]; - } else if ([value isKindOfClass:[NSArray class]]) { - typeMap[@"type"] = @"array"; - typeMap[@"value"] = [RNFirebaseFirestoreDocumentReference buildNativeArray:value]; - } else if ([value isKindOfClass:[FIRDocumentReference class]]) { - typeMap[@"type"] = @"reference"; - FIRDocumentReference *ref = (FIRDocumentReference *)value; - typeMap[@"value"] = [ref path]; - } else if ([value isKindOfClass:[FIRGeoPoint class]]) { - typeMap[@"type"] = @"geopoint"; - FIRGeoPoint *point = (FIRGeoPoint *)value; - NSMutableDictionary *geopoint = [[NSMutableDictionary alloc] init]; - geopoint[@"latitude"] = @([point latitude]); - geopoint[@"longitude"] = @([point longitude]); - typeMap[@"value"] = geopoint; - } else if ([value isKindOfClass:[NSDate class]]) { - typeMap[@"type"] = @"date"; - // NOTE: The round() is important as iOS ends up giving .999 otherwise, - // and loses a millisecond when going between native and JS - typeMap[@"value"] = @(round([(NSDate *)value timeIntervalSince1970] * 1000.0)); - } else if ([value isKindOfClass:[NSNumber class]]) { - NSNumber *number = (NSNumber *)value; - if (number == (void*)kCFBooleanFalse || number == (void*)kCFBooleanTrue) { - typeMap[@"type"] = @"boolean"; - } else { - typeMap[@"type"] = @"number"; - } - typeMap[@"value"] = value; - } else if ([value isKindOfClass:[NSData class]]) { - typeMap[@"type"] = @"blob"; - NSData *blob = (NSData *)value; - typeMap[@"value"] = [blob base64EncodedStringWithOptions:0]; - } else { - // TODO: Log an error - typeMap[@"type"] = @"null"; - } +/** + * + * @param value + * @return + */ ++ (NSDictionary *)buildTypeMap:(id)value { + NSMutableDictionary *typeMap = [[NSMutableDictionary alloc] init]; + // null + if (value == nil) { + typeMap[typeKey] = typeNull; return typeMap; + } + + // strings + if ([value isKindOfClass:[NSString class]]) { + typeMap[typeKey] = typeString; + typeMap[valueKey] = value; + return typeMap; + } + + // objects + if ([value isKindOfClass:[NSDictionary class]]) { + typeMap[typeKey] = typeObject; + typeMap[valueKey] = [RNFirebaseFirestoreDocumentReference buildNativeMap:value]; + return typeMap; + } + + // array + if ([value isKindOfClass:[NSArray class]]) { + typeMap[typeKey] = typeArray; + typeMap[valueKey] = [RNFirebaseFirestoreDocumentReference buildNativeArray:value]; + return typeMap; + } + + // reference + if ([value isKindOfClass:[FIRDocumentReference class]]) { + typeMap[typeKey] = typeReference; + FIRDocumentReference *ref = (FIRDocumentReference *) value; + typeMap[valueKey] = [ref path]; + return typeMap; + } + + // geopoint + if ([value isKindOfClass:[FIRGeoPoint class]]) { + typeMap[typeKey] = typeGeoPoint; + FIRGeoPoint *point = (FIRGeoPoint *) value; + NSMutableDictionary *geopoint = [[NSMutableDictionary alloc] init]; + geopoint[keyLatitude] = @([point latitude]); + geopoint[keyLongitude] = @([point longitude]); + typeMap[valueKey] = geopoint; + return typeMap; + } + + // date + if ([value isKindOfClass:[NSDate class]]) { + typeMap[typeKey] = typeDate; + // round is required otherwise iOS ends up with .999 and loses a millisecond + // when going between native and JS + typeMap[valueKey] = @(round([(NSDate *) value timeIntervalSince1970] * 1000.0)); + return typeMap; + } + + // number / boolean / infinity / nan + if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *number = (NSNumber *) value; + + // infinity + if (number == @(INFINITY)) { + typeMap[typeKey] = typeInfinity; + return typeMap; + } + + // boolean + if (number == [NSValue valueWithPointer:(void *) kCFBooleanFalse] + || number == [NSValue valueWithPointer:(void *) kCFBooleanTrue]) { + typeMap[typeKey] = typeBoolean; + typeMap[valueKey] = value; + return typeMap; + } + + // nan + if ([[value description].lowercaseString isEqual:@"nan"]) { + typeMap[typeKey] = typeNaN; + return typeMap; + } + + // number + typeMap[typeKey] = typeNumber; + typeMap[valueKey] = value; + return typeMap; + } + + // blobs (converted to base64) + if ([value isKindOfClass:[NSData class]]) { + NSData *blob = (NSData *) value; + typeMap[typeKey] = typeBlob; + typeMap[valueKey] = [blob base64EncodedStringWithOptions:0]; + return typeMap; + } + + DLog(@"RNFirebaseFirestore: Unsupported value sent to buildTypeMap - class type is %@", + NSStringFromClass([value class])); + + typeMap[typeKey] = typeNull; + return typeMap; } -+(NSDictionary *)parseJSMap:(FIRFirestore *) firestore - jsMap:(NSDictionary *) jsMap { - NSMutableDictionary* map = [[NSMutableDictionary alloc] init]; - if (jsMap) { - [jsMap enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { - map[key] = [RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:obj]; - }]; - } - return map; +/** + * + * @param firestore + * @param jsMap + * @return + */ ++ (NSDictionary *)parseJSMap:(FIRFirestore *)firestore + jsMap:(NSDictionary *)jsMap { + NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; + + if (jsMap) { + [jsMap enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) { + map[key] = [RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:obj]; + }]; + } + + return map; } -+(NSArray *)parseJSArray:(FIRFirestore *) firestore - jsArray:(NSArray *) jsArray { - NSMutableArray* array = [[NSMutableArray alloc] init]; - if (jsArray) { - [jsArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - [array addObject:[RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:obj]]; - }]; - } - return array; +/** + * + * @param firestore + * @param jsArray + * @return + */ ++ (NSArray *)parseJSArray:(FIRFirestore *)firestore + jsArray:(NSArray *)jsArray { + NSMutableArray *array = [[NSMutableArray alloc] init]; + + if (jsArray) { + [jsArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + [array addObject:[RNFirebaseFirestoreDocumentReference parseJSTypeMap:firestore jsTypeMap:obj]]; + }]; + } + + return array; } -+(id)parseJSTypeMap:(FIRFirestore *) firestore - jsTypeMap:(NSDictionary *) jsTypeMap { - NSString *type = jsTypeMap[@"type"]; - id value = jsTypeMap[@"value"]; - if ([type isEqualToString:@"array"]) { - return [RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:value]; - } else if ([type isEqualToString:@"object"]) { - return [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:value]; - } else if ([type isEqualToString:@"reference"]) { - return [firestore documentWithPath:value]; - } else if ([type isEqualToString:@"blob"]) { - return [[NSData alloc] initWithBase64EncodedString:(NSString *) value options:0]; - } else if ([type isEqualToString:@"geopoint"]) { - NSDictionary *geopoint = (NSDictionary*)value; - NSNumber *latitude = geopoint[@"latitude"]; - NSNumber *longitude = geopoint[@"longitude"]; - 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"]) { - 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 { - // TODO: Log error - return nil; +/** + * + * @param firestore + * @param jsTypeMap + * @return + */ ++ (id)parseJSTypeMap:(FIRFirestore *)firestore + jsTypeMap:(NSDictionary *)jsTypeMap { + id value = jsTypeMap[valueKey]; + NSString *type = jsTypeMap[typeKey]; + + if ([type isEqualToString:typeArray]) { + return [RNFirebaseFirestoreDocumentReference parseJSArray:firestore jsArray:value]; + } + + if ([type isEqualToString:typeObject]) { + return [RNFirebaseFirestoreDocumentReference parseJSMap:firestore jsMap:value]; + } + + if ([type isEqualToString:typeReference]) { + return [firestore documentWithPath:value]; + } + + if ([type isEqualToString:typeBlob]) { + return [[NSData alloc] initWithBase64EncodedString:(NSString *) value options:0]; + } + + if ([type isEqualToString:typeGeoPoint]) { + NSDictionary *geopoint = (NSDictionary *) value; + NSNumber *latitude = geopoint[keyLatitude]; + NSNumber *longitude = geopoint[keyLongitude]; + return [[FIRGeoPoint alloc] initWithLatitude:[latitude doubleValue] longitude:[longitude doubleValue]]; + } + + if ([type isEqualToString:typeDate]) { + return [NSDate dateWithTimeIntervalSince1970:([(NSNumber *) value doubleValue] / 1000.0)]; + } + + if ([type isEqualToString:typeDocumentId]) { + return [FIRFieldPath documentID]; + } + + if ([type isEqualToString:typeFieldValue]) { + NSString *string = (NSString *) value; + + if ([string isEqualToString:typeDelete]) { + return [FIRFieldValue fieldValueForDelete]; } + + if ([string isEqualToString:typeTimestamp]) { + return [FIRFieldValue fieldValueForServerTimestamp]; + } + + DLog(@"RNFirebaseFirestore: Unsupported field-value sent to parseJSTypeMap - value is %@", + NSStringFromClass([value class])); + + return nil; + } + + if ([type isEqualToString:typeInfinity]) { + return @(INFINITY); + } + + if ([type isEqualToString:typeNaN]) { + return [NSDecimalNumber notANumber]; + } + + if ([type isEqualToString:typeBoolean] || [type isEqualToString:typeNumber] || [type isEqualToString:typeString] + || [type isEqualToString:typeNull]) { + return value; + } + + return nil; } #endif diff --git a/src/modules/firestore/firestoreTypes.flow.js b/src/modules/firestore/firestoreTypes.flow.js index 22f2509c..22276a2a 100644 --- a/src/modules/firestore/firestoreTypes.flow.js +++ b/src/modules/firestore/firestoreTypes.flow.js @@ -46,6 +46,8 @@ export type NativeDocumentSnapshot = { export type NativeTypeMap = { type: + | 'nan' + | 'infinity' | 'array' | 'boolean' | 'date' diff --git a/src/modules/firestore/utils/serialize.js b/src/modules/firestore/utils/serialize.js index 41a0a85d..8f1fb3b8 100644 --- a/src/modules/firestore/utils/serialize.js +++ b/src/modules/firestore/utils/serialize.js @@ -50,42 +50,63 @@ export const buildNativeArray = (array: Object[]): NativeTypeMap[] => { export const buildTypeMap = (value: any): NativeTypeMap | null => { const type = typeOf(value); - if (value === null || value === undefined || Number.isNaN(value)) { + + if (Number.isNaN(value)) { + return { + type: 'nan', + value: null, + }; + } + + if (value === Infinity) { + return { + type: 'infinity', + value: null, + }; + } + + if (value === null || value === undefined) { return { type: 'null', value: null, }; } + if (value === DELETE_FIELD_VALUE) { return { type: 'fieldvalue', value: 'delete', }; } + if (value === SERVER_TIMESTAMP_FIELD_VALUE) { return { type: 'fieldvalue', value: 'timestamp', }; } + if (value === DOCUMENT_ID) { return { type: 'documentid', value: null, }; } + if (type === 'boolean' || type === 'number' || type === 'string') { return { type, value, }; } + if (type === 'array') { return { type, value: buildNativeArray(value), }; } + if (type === 'object') { if (value instanceof DocumentReference) { return { @@ -93,6 +114,7 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => { value: value.path, }; } + if (value instanceof GeoPoint) { return { type: 'geopoint', @@ -102,23 +124,27 @@ export const buildTypeMap = (value: any): NativeTypeMap | null => { }, }; } + if (value instanceof Date) { return { type: 'date', value: value.getTime(), }; } + if (value instanceof Blob) { return { type: 'blob', value: value.toBase64(), }; } + return { type: 'object', value: buildNativeMap(value), }; } + console.warn(`Unknown data type received ${type}`); return null; }; @@ -160,27 +186,43 @@ const parseTypeMap = (firestore: Firestore, typeMap: NativeTypeMap): any => { if (type === 'null') { return null; } + if (type === 'boolean' || type === 'number' || type === 'string') { return value; } + if (type === 'array') { return parseNativeArray(firestore, value); } + if (type === 'object') { return parseNativeMap(firestore, value); } + if (type === 'reference') { return new DocumentReference(firestore, Path.fromName(value)); } + if (type === 'geopoint') { return new GeoPoint(value.latitude, value.longitude); } + if (type === 'date') { return new Date(value); } + if (type === 'blob') { return Blob.fromBase64String(value); } + + if (type === 'infinity') { + return Infinity; + } + + if (type === 'nan') { + return NaN; + } + console.warn(`Unknown data type received ${type}`); return value; }; diff --git a/tests/helpers/firestore.js b/tests/helpers/firestore.js index 12b26f54..91e2bbfa 100644 --- a/tests/helpers/firestore.js +++ b/tests/helpers/firestore.js @@ -41,6 +41,8 @@ module.exports = { gaz: 12.1234567, geopoint: new firebase.firestore.GeoPoint(0, 0), naz: null, + nan: NaN, + infinity: Infinity, arrNumber: [1, 2, 3, 4], arrString: ['a', 'b', 'c', 'd'], object: {