From a8948ced263e7d3ce870dff47cf7c29b12806247 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 4 Dec 2017 16:42:43 -0800 Subject: Adding SnapshotOptions to deal with pending ServerTimestamps --- Firestore/Source/API/FIRDocumentSnapshot.m | 63 ++++++++++++---- Firestore/Source/API/FIRSetOptions.m | 1 - Firestore/Source/API/FIRSnapshotOptions+Internal.h | 38 ++++++++++ Firestore/Source/API/FIRSnapshotOptions.m | 83 ++++++++++++++++++++ Firestore/Source/Model/FSTFieldValue.h | 66 ++++++++++++---- Firestore/Source/Model/FSTFieldValue.m | 80 +++++++++++++++----- Firestore/Source/Model/FSTMutation.h | 8 +- Firestore/Source/Model/FSTMutation.m | 29 +++++-- Firestore/Source/Model/FSTMutationBatch.m | 2 + Firestore/Source/Public/FIRDocumentSnapshot.h | 88 ++++++++++++++++++++++ 10 files changed, 400 insertions(+), 58 deletions(-) create mode 100644 Firestore/Source/API/FIRSnapshotOptions+Internal.h create mode 100644 Firestore/Source/API/FIRSnapshotOptions.m (limited to 'Firestore/Source') diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m index b78472e..0d60033 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -20,6 +20,7 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Model/FSTDatabaseID.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -100,6 +101,10 @@ NS_ASSUME_NONNULL_BEGIN } - (NSDictionary *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} + +- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { FSTDocument *document = self.internalDocument; if (!document) { @@ -110,29 +115,38 @@ NS_ASSUME_NONNULL_BEGIN self.internalKey); } - return [self convertedObject:[self.internalDocument data]]; + return [self convertedObject:[self.internalDocument data] + options:[self convertedSnapshotOptions:options]]; } -- (nullable id)objectForKeyedSubscript:(id)key { +- (nullable id)valueForField:(id)field { + return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { FIRFieldPath *fieldPath; - if ([key isKindOfClass:[NSString class]]) { - fieldPath = [FIRFieldPath pathWithDotSeparatedString:key]; - } else if ([key isKindOfClass:[FIRFieldPath class]]) { - fieldPath = key; + if ([field isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = field; } else { FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); } FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return [self convertedValue:fieldValue]; + return [self convertedValue:fieldValue options:[self convertedSnapshotOptions:options]]; } -- (id)convertedValue:(FSTFieldValue *)value { +- (nullable id)objectForKeyedSubscript:(id)key { + return [self valueForField:key]; +} + +- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { if ([value isKindOfClass:[FSTObjectValue class]]) { - return [self convertedObject:(FSTObjectValue *)value]; + return [self convertedObject:(FSTObjectValue *)value options:options]; } else if ([value isKindOfClass:[FSTArrayValue class]]) { - return [self convertedArray:(FSTArrayValue *)value]; + return [self convertedArray:(FSTArrayValue *)value options:options]; } else if ([value isKindOfClass:[FSTReferenceValue class]]) { FSTReferenceValue *ref = (FSTReferenceValue *)value; FSTDatabaseID *refDatabase = ref.databaseID; @@ -146,30 +160,47 @@ NS_ASSUME_NONNULL_BEGIN self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, database.databaseID); } - return [FIRDocumentReference referenceWithKey:ref.value firestore:self.firestore]; + return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] + firestore:self.firestore]; } else { - return value.value; + return [value valueWithOptions:options]; } } -- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue { +- (NSDictionary *)convertedObject:(FSTObjectValue *)objectValue + options:(FSTFieldValueOptions *)options { NSMutableDictionary *result = [NSMutableDictionary dictionary]; [objectValue.internalValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { - result[key] = [self convertedValue:value]; + result[key] = [self convertedValue:value options:options]; }]; return result; } -- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue { +- (NSArray *)convertedArray:(FSTArrayValue *)arrayValue + options:(FSTFieldValueOptions *)options { NSArray *internalValue = arrayValue.internalValue; NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { - [result addObject:[self convertedValue:value]]; + [result addObject:[self convertedValue:value options:options]]; }]; return result; } +/** Create a field value option from a snapshot option. */ +- (FSTFieldValueOptions *)convertedSnapshotOptions:(FIRSnapshotOptions *)snapshotOptions { + switch (snapshotOptions.serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; + default: + return [FSTFieldValueOptions defaultOptions]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m index 623deaa..743bcc7 100644 --- a/Firestore/Source/API/FIRSetOptions.m +++ b/Firestore/Source/API/FIRSetOptions.m @@ -15,7 +15,6 @@ */ #import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/Model/FSTMutation.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/Source/API/FIRSnapshotOptions+Internal.h b/Firestore/Source/API/FIRSnapshotOptions+Internal.h new file mode 100644 index 0000000..de2aaa7 --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions+Internal.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRDocumentSnapshot.h" + +#import + +@class FIRSnapshotOptions; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions (Internal) + +/** Returns a default instance of FIRSnapshotOptions that specifies no options. */ ++ (instancetype)defaultOptions; + +/* Initializes a new instance with the specified server timestamp behavior. */ +- (instancetype)initWithServerTimestampBehavior:(int)serverTimestampBehavior; + +/* Returns the server timestamp behavior. Returns -1 if no behavior is specified. */ +- (int)serverTimestampBehavior; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions.m b/Firestore/Source/API/FIRSnapshotOptions.m new file mode 100644 index 0000000..adc4a65 --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions.m @@ -0,0 +1,83 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRDocumentSnapshot.h" + +#import "FIRDocumentSnapshot+Internal.h" +#import "FSTAssert.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Model/FSTDatabaseID.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTFieldValue.h" +#import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The default server timestamp behavior (returning NSNull for pending timestamps). */ +static const int kFIRServerTimestampBehaviorDefault = -1; + +@interface FIRSnapshotOptions () + +@property(nonatomic) int serverTimestampBehavior; + +@end + +@implementation FIRSnapshotOptions + +- (instancetype)initWithServerTimestampBehavior:(int)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + + return self; +} + ++ (instancetype)defaultOptions { + static FIRSnapshotOptions *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:kFIRServerTimestampBehaviorDefault]; + }); + + return sharedInstance; +} + ++ (instancetype)setServerTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { + switch (serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FIRServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FIRServerTimestampBehaviorPrevious]; + default: + FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); + } +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h index 6de9793..969b3b0 100644 --- a/Firestore/Source/Model/FSTFieldValue.h +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -22,6 +22,7 @@ @class FSTDocumentKey; @class FSTFieldPath; @class FSTTimestamp; +@class FSTFieldValueOptions; @class FIRGeoPoint; NS_ASSUME_NONNULL_BEGIN @@ -40,6 +41,30 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { FSTTypeOrderObject, }; +/** Defines the return value for pending server timestamps. */ +typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { + FSTServerTimestampBehaviorDefault, + FSTServerTimestampBehaviorEstimate, + FSTServerTimestampBehaviorPrevious, +}; + +/** Holds properties that define field value deserialization options. */ +@interface FSTFieldValueOptions : NSObject + +@property(nonatomic, readonly) FSTServerTimestampBehavior serverTimestampBehavior; + +- (instancetype)init NS_UNAVAILABLE; + +/** Creates a FSTFieldValueOptions instance that specifies deserialization behavior for pending + * server timestamps. */ +- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior + NS_DESIGNATED_INITIALIZER; + +/** Returns the default deserialization options. */ ++ (instancetype)defaultOptions; + +@end + /** * Abstract base class representing an immutable data value as stored in Firestore. FSTFieldValue * represents all the different kinds of values that can be stored in fields in a document. @@ -71,6 +96,14 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ - (id)value; +/** + * Converts an FSTFieldValue into the value that users will see in document snapshots. + * + * Options can be provided to configure the deserialization of some field values (such as server + * timestamps). + */ +- (id)valueWithOptions:(FSTFieldValueOptions *)options; + /** Compares against another FSTFieldValue. */ - (NSComparisonResult)compare:(FSTFieldValue *)other; @@ -81,7 +114,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTNullValue : FSTFieldValue + (instancetype)nullValue; -- (id)value; +- (id)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -91,7 +124,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { + (instancetype)trueValue; + (instancetype)falseValue; + (instancetype)booleanValue:(BOOL)value; -- (NSNumber *)value; +- (NSNumber *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -106,8 +139,8 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTIntegerValue : FSTNumberValue + (instancetype)integerValue:(int64_t)value; -- (NSNumber *)value; - (int64_t)internalValue; +- (NSNumber *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -116,8 +149,8 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { @interface FSTDoubleValue : FSTNumberValue + (instancetype)doubleValue:(double)value; + (instancetype)nanValue; -- (NSNumber *)value; - (double)internalValue; +- (NSNumber *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -125,7 +158,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTStringValue : FSTFieldValue + (instancetype)stringValue:(NSString *)value; -- (NSString *)value; +- (NSString *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -133,7 +166,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTTimestampValue : FSTFieldValue + (instancetype)timestampValue:(FSTTimestamp *)value; -- (NSDate *)value; +- (NSDate *)valueWithOptions:(FSTFieldValueOptions *)options; - (FSTTimestamp *)internalValue; @end @@ -144,15 +177,18 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * - FSTServerTimestampValue instances are created as the result of applying an FSTTransformMutation * (see [FSTTransformMutation applyTo]). They can only exist in the local view of a document. * Therefore they do not need to be parsed or serialized. - * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they evaluate to NSNull (at least - * for now, see b/62064202). + * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they by default evaluate to NSNull. + * This behavior can be configured by passing custom FSTFieldValueOptions to `valueWithOptions:`. * - They sort after all FSTTimestampValues. With respect to other FSTServerTimestampValues, they * sort by their localWriteTime. */ @interface FSTServerTimestampValue : FSTFieldValue -+ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime; -- (NSNull *)value; ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue; + @property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@property(nonatomic, strong, readonly, nullable) FSTFieldValue *previousValue; + @end /** @@ -160,7 +196,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTGeoPointValue : FSTFieldValue + (instancetype)geoPointValue:(FIRGeoPoint *)value; -- (FIRGeoPoint *)value; +- (FIRGeoPoint *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -168,7 +204,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTBlobValue : FSTFieldValue + (instancetype)blobValue:(NSData *)value; -- (NSData *)value; +- (NSData *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** @@ -176,7 +212,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTReferenceValue : FSTFieldValue + (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID; -- (FSTDocumentKey *)value; +- (FSTDocumentKey *)valueWithOptions:(FSTFieldValueOptions *)options; @property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; @end @@ -200,7 +236,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { - (instancetype)init NS_UNAVAILABLE; -- (NSDictionary *)value; +- (NSDictionary *)valueWithOptions:(FSTFieldValueOptions *)options; - (FSTImmutableSortedDictionary *)internalValue; /** Returns the value at the given path if it exists. Returns nil otherwise. */ @@ -234,7 +270,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { - (instancetype)init NS_UNAVAILABLE; -- (NSArray *)value; +- (NSArray *)valueWithOptions:(FSTFieldValueOptions *)options; - (NSArray *)internalValue; @end diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m index 95ad306..65478ce 100644 --- a/Firestore/Source/Model/FSTFieldValue.m +++ b/Firestore/Source/Model/FSTFieldValue.m @@ -27,6 +27,34 @@ NS_ASSUME_NONNULL_BEGIN +#pragma mark - FSTFieldValueOptions + +@implementation FSTFieldValueOptions + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + return self; +} + ++ (instancetype)defaultOptions { + static FSTFieldValueOptions *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorDefault]; + }); + + return sharedInstance; +} + +@end + #pragma mark - FSTFieldValue @interface FSTFieldValue () @@ -40,6 +68,10 @@ NS_ASSUME_NONNULL_BEGIN } - (id)value { + return [self valueWithOptions:[FSTFieldValueOptions defaultOptions]]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { @throw FSTAbstractMethodException(); // NOLINT } @@ -89,7 +121,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderNull; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return [NSNull null]; } @@ -155,7 +187,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBoolean; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue ? @YES : @NO; } @@ -233,7 +265,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -285,7 +317,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -332,7 +364,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderString; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -379,7 +411,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderTimestamp; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { // For developers, we expose Timestamps as Dates. return self.internalValue.approximateDateValue; } @@ -410,14 +442,18 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTServerTimestampValue -+ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime { - return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime]; ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { + return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime + previousValue:previousValue]; } -- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime { +- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { self = [super init]; if (self) { _localWriteTime = localWriteTime; + _previousValue = previousValue; } return self; } @@ -426,9 +462,17 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderTimestamp; } -- (NSNull *)value { - // For developers, server timestamps always evaluate to NSNull (for now, at least; b/62064202). - return [NSNull null]; +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + switch (options.serverTimestampBehavior) { + case FSTServerTimestampBehaviorDefault: + return [NSNull null]; + case FSTServerTimestampBehaviorEstimate: + return [self.localWriteTime approximateDateValue]; + case FSTServerTimestampBehaviorPrevious: + return self.previousValue ? [self.previousValue valueWithOptions:options] : [NSNull null]; + default: + FSTFail(@"Unexpected server timestamp option: %d", (int)options.serverTimestampBehavior); + } } - (BOOL)isEqual:(id)other { @@ -481,7 +525,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderGeoPoint; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -529,7 +573,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBlob; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -573,7 +617,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.key; } @@ -648,11 +692,11 @@ NS_ASSUME_NONNULL_BEGIN return [self initWithImmutableDictionary:dictionary]; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { NSMutableDictionary *result = [NSMutableDictionary dictionary]; [self.internalValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { - result[key] = [obj value]; + result[key] = [obj valueWithOptions:options]; }]; return result; } @@ -803,7 +847,7 @@ NS_ASSUME_NONNULL_BEGIN return [self.internalValue hash]; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { [result addObject:[obj value]]; diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h index ef7f1c8..b2e7f5e 100644 --- a/Firestore/Source/Model/FSTMutation.h +++ b/Firestore/Source/Model/FSTMutation.h @@ -158,8 +158,10 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * Applies this mutation to the given FSTDocument, FSTDeletedDocument or nil, if we don't have * information about this document. Both the input and returned documents can be nil. * - * @param maybeDoc The document to mutate. The input document should nil if it does not currently - * exist. + * @param maybeDoc The current state of the document to mutate. The input document should be nil if + * it does not currently exist. + * @param baseDoc The state of the document prior to this mutation batch. The input document should + * be nil if it the document did not exist. * @param localWriteTime A timestamp indicating the local write time of the batch this mutation is * a part of. * @param mutationResult Optional result info from the backend. If omitted, it's assumed that @@ -197,6 +199,7 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * FSTSetMutation, but not necessarily for an FSTPatchMutation). */ - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult; @@ -205,6 +208,7 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * backend). */ - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime; @property(nonatomic, strong, readonly) FSTDocumentKey *key; diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m index 5b47280..2685990 100644 --- a/Firestore/Source/Model/FSTMutation.m +++ b/Firestore/Source/Model/FSTMutation.m @@ -237,14 +237,16 @@ NS_ASSUME_NONNULL_BEGIN } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult { @throw FSTAbstractMethodException(); // NOLINT } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime { - return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil]; + return [self applyTo:maybeDoc baseDoc:baseDoc localWriteTime:localWriteTime mutationResult:nil]; } @end @@ -288,6 +290,7 @@ NS_ASSUME_NONNULL_BEGIN } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult { if (mutationResult) { @@ -363,6 +366,7 @@ NS_ASSUME_NONNULL_BEGIN } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult { if (mutationResult) { @@ -452,6 +456,7 @@ NS_ASSUME_NONNULL_BEGIN } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult { if (mutationResult) { @@ -473,8 +478,9 @@ NS_ASSUME_NONNULL_BEGIN BOOL hasLocalMutations = (mutationResult == nil); NSArray *transformResults = - mutationResult ? mutationResult.transformResults - : [self localTransformResultsWithWriteTime:localWriteTime]; + mutationResult + ? mutationResult.transformResults + : [self localTransformResultsWithPreviousDocument:baseDoc writeTime:localWriteTime]; FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; return [FSTDocument documentWithData:newData key:doc.key @@ -486,16 +492,26 @@ NS_ASSUME_NONNULL_BEGIN * Creates an array of "transform results" (a transform result is a field value representing the * result of applying a transform) for use when applying an FSTTransformMutation locally. * + * @param previousDocument The document prior to applying this mutation batch. * @param localWriteTime The local time of the transform mutation (used to generate * FSTServerTimestampValues). * @return The transform results array. */ -- (NSArray *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime { +- (NSArray *) +localTransformResultsWithPreviousDocument:(FSTMaybeDocument *_Nullable)previousDocument + writeTime:(FSTTimestamp *)localWriteTime { NSMutableArray *transformResults = [NSMutableArray array]; for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { - [transformResults addObject:[FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:localWriteTime]]; + FSTFieldValue *previousValue = nil; + + if (previousDocument && [previousDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)previousDocument) fieldForPath:fieldTransform.path]; + } + + [transformResults + addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]]; } else { FSTFail(@"Encountered unknown transform: %@", fieldTransform); } @@ -552,6 +568,7 @@ NS_ASSUME_NONNULL_BEGIN } - (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + baseDoc:(FSTMaybeDocument *_Nullable)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime mutationResult:(FSTMutationResult *_Nullable)mutationResult { if (mutationResult) { diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m index 3677908..1d16de5 100644 --- a/Firestore/Source/Model/FSTMutationBatch.m +++ b/Firestore/Source/Model/FSTMutationBatch.m @@ -71,6 +71,7 @@ const FSTBatchID kFSTBatchIDUnknown = -1; mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); + FSTMaybeDocument *baseDoc = maybeDoc; if (mutationBatchResult) { FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, @"Mismatch between mutations length (%lu) and results length (%lu)", @@ -83,6 +84,7 @@ const FSTBatchID kFSTBatchIDUnknown = -1; FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; if ([mutation.key isEqualToKey:documentKey]) { maybeDoc = [mutation applyTo:maybeDoc + baseDoc:baseDoc localWriteTime:self.localWriteTime mutationResult:mutationResult]; } diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 3e67c25..f1ae6d8 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -21,6 +21,49 @@ NS_ASSUME_NONNULL_BEGIN +/** + * Controls the return value for server timestamps that have not yet been set to + * their final value. + */ +typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) { + /** + * Return a local estimates for `FieldValue.serverTimestamp()` + * fields that have not yet been set to their final value. This estimate will + * likely differ from the final value and may cause these pending values to + * change once the server result becomes available. + */ + FIRServerTimestampBehaviorEstimate, + + /** + * Return the previous value for `FieldValue.serverTimestamp()` fields that + * have not yet been set to their final value. + */ + FIRServerTimestampBehaviorPrevious, +}; + +/** + * Options that configure how data is retrieved from a `DocumentSnapshot` + * (e.g. the desired behavior for server timestamps that have not yet been set + * to their final value). + */ +NS_SWIFT_NAME(SnapshotOptions) +@interface FIRSnapshotOptions : NSObject + +/** */ +- (instancetype)init __attribute__((unavailable("FIRSnapshotOptions cannot be created directly."))); + +/** + * If set, controls the return value for `FieldValue.serverTimestamp()` + * fields that have not yet been set to their final value. + * + * If omitted, `NSNull` will be returned by default. + * + * @return The created `FIRSnapshotOptions` object. + */ ++ (instancetype)setServerTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior; + +@end + /** * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data * can be extracted with the `data` property or by using subscript syntax to access a specific @@ -48,10 +91,55 @@ NS_SWIFT_NAME(DocumentSnapshot) /** * Retrieves all fields in the document as an `NSDictionary`. * + * Server-provided timestamps that have not yet been set to their final value + * will be returned as `NSNull`. You can use `dataWithOptions()` to configure this + * behavior. + * * @return An `NSDictionary` containing all fields in the document. */ - (NSDictionary *)data; +/** + * Retrieves all fields in the document as a `Dictionary`. + * + * @param options `SnapshotOptions` to configure how data is returned from + * the snapshot (e.g. the desired behavior for server timestamps that have not + * yet been set to their final value). + * @return A `Dictionary` containing all fields in the document. + */ +- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options; + +/** + * Retrieves a specific field from the document. + * + * The timestamps that have not yet been set to their final value + * will be returned as `NSNull`. The can use `get(_:options:)` to + * configure this behavior. + * + * @param field The field to retrieve. + * @return The value contained in the field or `nil` if the field doesn't exist. + */ +- (nullable id)valueForField:(id)field NS_SWIFT_NAME(get(_:)); + +/** + * Retrieves a specific field from the document. + * + * The timestamps that have not yet been set to their final value + * will be returned as `NSNull`. The can use `get(_:options:)` to + * configure this behavior. + * + * @param field The field to retrieve. + * @param options `SnapshotOptions` to configure how data is returned from + * the snapshot (e.g. the desired behavior for server timestamps that have not + * yet been set to their final value). + * @return The value contained in the field or `nil` if the field doesn't exist. + */ +// clang-format off +- (nullable id)valueForField:(id)field + options:(FIRSnapshotOptions *)options + NS_SWIFT_NAME(get(_:options:)); +// clang-format on + /** * Retrieves a specific field from the document. * -- cgit v1.2.3