diff options
18 files changed, 611 insertions, 99 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 437b661..48bb327 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -917,8 +917,7 @@ inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", - "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac-f0850809/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac-Defines-NSData+zlib/GoogleToolboxForMac.framework", "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", @@ -930,7 +929,6 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m index 2ee3966..5b210d7 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m @@ -16,9 +16,13 @@ @import FirebaseFirestore; +#import <FirebaseFirestore/FIRDocumentReference.h> +#import <FirebaseFirestore/FIRDocumentSnapshot.h> +#import <Firestore/Source/Core/FSTFirestoreClient.h> +#import <OCMock/OCMArg.h> #import <XCTest/XCTest.h> -#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @@ -42,11 +46,20 @@ // Listener registration for a listener maintained during the course of the test. id<FIRListenerRegistration> _listenerRegistration; + + // Snapshot options that return the previous value for pending server timestamps. + FIRSnapshotOptions *_previousValue; + FIRSnapshotOptions *_estimateValue; } - (void)setUp { [super setUp]; + _previousValue = + [FIRSnapshotOptions setServerTimestampBehavior:FIRServerTimestampBehaviorPrevious]; + _estimateValue = + [FIRSnapshotOptions setServerTimestampBehavior:FIRServerTimestampBehaviorEstimate]; + // Data written in tests via set. _setData = @{ @"a" : @42, @@ -63,7 +76,7 @@ _docRef = [self documentRef]; _accumulator = [FSTEventAccumulator accumulatorForTest:self]; - _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler]; + _listenerRegistration = [_docRef addSnapshotListener:_accumulator.valueEventHandler]; // Wait for initial nil snapshot to avoid potential races. FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; @@ -76,7 +89,9 @@ [super tearDown]; } -// Returns the expected data, with an arbitrary timestamp substituted in. +#pragma mark - Test Helpers + +/** Returns the expected data, with the specified timestamp substituted in. */ - (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp { return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; } @@ -88,14 +103,46 @@ XCTAssertEqualObjects(initialDataSnap.data, _initialData); } +/** Waits for a snapshot with local writes. */ +- (FIRDocumentSnapshot *)waitForLocalEvent { + return [_accumulator awaitEventWithName:@"Local event."]; +} + /** Waits for a snapshot containing _setData but with NSNull for the timestamps. */ -- (void)waitForLocalEvent { - FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."]; +- (FIRDocumentSnapshot *)waitForLocalEventWithNullTimestamps { + FIRDocumentSnapshot *localSnap = [self waitForLocalEvent]; XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]); + return localSnap; +} + +/** Waits for a snapshot containing _setData but with a local estimate for the timestamps. */ +- (FIRDocumentSnapshot *)waitForLocalEventWithEstimatedTimestamps { + FIRDocumentSnapshot *localSnap = [self waitForLocalEvent]; + id timestamp = [localSnap valueForField:@"when" options:_estimateValue]; + XCTAssertTrue([timestamp isKindOfClass:[NSDate class]]); + XCTAssertEqualObjects([localSnap dataWithOptions:_estimateValue], + [self expectedDataWithTimestamp:timestamp]); + return localSnap; +} + +/** Waits for a snapshot containing _setData but using the previous field value for the timestamps. */ +- (FIRDocumentSnapshot *)waitForLocalEventWithPreviousDataFromSnapshot: + (FIRDocumentSnapshot *_Nullable)previousSnapshot { + FIRDocumentSnapshot *localSnap = [self waitForLocalEvent]; + + if (previousSnapshot == nil) { + XCTAssertEqualObjects([localSnap dataWithOptions:_previousValue], + [self expectedDataWithTimestamp:[NSNull null]]); + } else { + XCTAssertEqualObjects([localSnap dataWithOptions:_previousValue], + [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); + } + + return localSnap; } /** Waits for a snapshot containing _setData but with resolved server timestamps. */ -- (void)waitForRemoteEvent { +- (FIRDocumentSnapshot *)waitForRemoteEvent { // server event should have a resolved timestamp; verify it. FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"]; XCTAssertTrue(remoteSnap.exists); @@ -106,8 +153,10 @@ // Validate the rest of the document. XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]); + return remoteSnap; } +/** Runs a transaction block. */ - (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock { XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { @@ -121,17 +170,114 @@ [self awaitExpectations]; } +/** Disables the network synchronously. */ +- (void)disableNetwork { + [_docRef.firestore.client disableNetworkWithCompletion:_accumulator.errorEventHandler]; + [_accumulator awaitEventWithName:@"Disconnect event."]; +} + +/** Enables the network synchronously. */ +- (void)enableNetwork { + [_docRef.firestore.client enableNetworkWithCompletion:_accumulator.errorEventHandler]; + [_accumulator awaitEventWithName:@"Reconnect event."]; +} + +#pragma mark - Test Cases + - (void)testServerTimestampsWorkViaSet { [self writeDocumentRef:_docRef data:_setData]; - [self waitForLocalEvent]; + [self waitForLocalEventWithNullTimestamps]; [self waitForRemoteEvent]; } - (void)testServerTimestampsWorkViaUpdate { [self writeInitialData]; [self updateDocumentRef:_docRef data:_updateData]; + [self waitForLocalEventWithNullTimestamps]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWithEstimatedValue { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEventWithEstimatedTimestamps]; + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWithPreviousValue { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEventWithPreviousDataFromSnapshot:nil]; + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + + [_docRef updateData:_updateData]; + [self waitForLocalEventWithPreviousDataFromSnapshot:remoteSnapshot]; + + [self waitForRemoteEvent]; +} + +- (void)testServerTimestampsWithPreviousValueOfDifferentType { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_previousValue], @42); + XCTAssertTrue( + [[localSnapshot valueForField:@"a" options:_estimateValue] isKindOfClass:[NSDate class]]); + + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitEventWithName:@"Remote event"]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); + XCTAssertTrue( + [[remoteSnapshot valueForField:@"a" options:_previousValue] isKindOfClass:[NSDate class]]); + XCTAssertTrue( + [[remoteSnapshot valueForField:@"a" options:_estimateValue] isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsWithConsecutiveUpdates { + [self writeDocumentRef:_docRef data:_setData]; [self waitForLocalEvent]; [self waitForRemoteEvent]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_previousValue], @42); + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_previousValue], @42); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitEventWithName:@"Remote event"]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsPreviousValueFromLocalMutation { + [self writeDocumentRef:_docRef data:_setData]; + [self waitForLocalEvent]; + [self waitForRemoteEvent]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_previousValue], @42); + + [_docRef updateData:@{ @"a" : @1337 }]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a"], @1337); + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_previousValue], @1337); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitEventWithName:@"Remote event"]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); } - (void)testServerTimestampsWorkViaTransactionSet { diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m index 562c29f..7dd9d4a 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m @@ -131,7 +131,7 @@ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] includeQueryMetadataChanges:YES] - listener:accumulator.handler]; + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); @@ -161,7 +161,7 @@ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] includeQueryMetadataChanges:YES] - listener:accumulator.handler]; + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); @@ -195,7 +195,7 @@ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options] includeQueryMetadataChanges:YES] - listener:accumulator.handler]; + listener:accumulator.valueEventHandler]; FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertEqual(initialSnap.count, 0); @@ -227,7 +227,7 @@ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self]; [doc addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES] - listener:accumulator.handler]; + listener:accumulator.valueEventHandler]; FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"]; XCTAssertFalse(initialSnap.exists); diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m index 847474a..ad75e50 100644 --- a/Firestore/Example/Tests/Integration/FSTSmokeTests.m +++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.m @@ -48,7 +48,7 @@ [self writeDocumentRef:writerRef data:data]; id<FIRListenerRegistration> listenerRegistration = - [readerRef addSnapshotListener:self.eventAccumulator.handler]; + [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"]; XCTAssertEqual([doc class], [FIRDocumentSnapshot class]); @@ -62,7 +62,7 @@ [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef, FIRDocumentReference *writerRef) { id<FIRListenerRegistration> listenerRegistration = - [readerRef addSnapshotListener:self.eventAccumulator.handler]; + [readerRef addSnapshotListener:self.eventAccumulator.valueEventHandler]; FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"]; XCTAssertFalse(doc1.exists); @@ -82,7 +82,7 @@ - (void)testWillFireValueEventsForEmptyCollections { FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"]; id<FIRListenerRegistration> listenerRegistration = - [collection addSnapshotListener:self.eventAccumulator.handler]; + [collection addSnapshotListener:self.eventAccumulator.valueEventHandler]; FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"]; XCTAssertEqual([snap class], [FIRQuerySnapshot class]); diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m index acf95f0..54d2ad5 100644 --- a/Firestore/Example/Tests/Model/FSTFieldValueTests.m +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m @@ -39,10 +39,12 @@ NSArray *FSTWrapGroups(NSArray *groups) { // strings that can be used instead. if ([value isEqual:@"server-timestamp-1"]) { wrappedValue = [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0)]; + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0) + previousValue:nil]; } else if ([value isEqual:@"server-timestamp-2"]) { wrappedValue = [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0)]; + serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0) + previousValue:nil]; } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { // We directly convert these here so that the databaseIDs can be different. FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; @@ -441,12 +443,15 @@ union DoubleBits { @[ // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue(). [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]], + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] + previousValue:nil], [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]] + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1] + previousValue:nil] ], @[ [FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2]] ], + serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2] + previousValue:nil] ], @[ FSTTestFieldValue(FSTTestGeoPoint(0, 1)), [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)] diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m index 678755e..4c5a4ad 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.m +++ b/Firestore/Example/Tests/Model/FSTMutationTests.m @@ -42,7 +42,7 @@ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"}); - FSTMaybeDocument *setDoc = [set applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *setDoc = [set applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"bar" : @"bar-value"}; XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -54,7 +54,7 @@ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -70,7 +70,7 @@ fieldMask:mask value:[FSTObjectValue objectValue] precondition:[FSTPrecondition none]]; - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -82,7 +82,7 @@ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -91,7 +91,7 @@ - (void)testPatchingDeletedDocumentsDoesNothing { FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0); FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil); - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(patchedDoc, baseDoc); } @@ -100,7 +100,8 @@ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); - FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *transformedDoc = + [transform applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; // Server timestamps aren't parsed, so we manually insert it. FSTObjectValue *expectedData = FSTTestObjectValue( @@ -108,7 +109,8 @@ @"baz" : @"baz-value" }); expectedData = [expectedData objectBySettingValue:[FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:_timestamp] + serverTimestampValueWithLocalWriteTime:_timestamp + previousValue:nil] forPath:FSTTestFieldPath(@"foo.bar")]; FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData @@ -129,8 +131,10 @@ initWithVersion:FSTTestVersion(1) transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; - FSTMaybeDocument *transformedDoc = - [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc + baseDoc:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, @@ -143,7 +147,7 @@ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key"); - FSTMaybeDocument *result = [mutation applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *result = [mutation applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); } @@ -155,7 +159,7 @@ FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; FSTMaybeDocument *setDoc = - [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + [set applyTo:baseDoc baseDoc:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); @@ -168,8 +172,10 @@ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; - FSTMaybeDocument *patchedDoc = - [patch applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc + baseDoc:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); @@ -179,8 +185,10 @@ do { \ FSTMutationResult *mutationResult = \ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ - FSTMaybeDocument *actual = \ - [mutation applyTo:base localWriteTime:_timestamp mutationResult:mutationResult]; \ + FSTMaybeDocument *actual = [mutation applyTo:base \ + baseDoc:base \ + localWriteTime:_timestamp \ + mutationResult:mutationResult]; \ XCTAssertEqualObjects(actual, expected); \ } while (0); diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h index ae5392c..e32f08b 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.h +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h @@ -23,7 +23,8 @@ NS_ASSUME_NONNULL_BEGIN -typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error); +typedef void (^FSTValueEventHandler)(id _Nullable, NSError *error); +typedef void (^FSTErrorEventHandler)(NSError *error); @interface FSTEventAccumulator : NSObject @@ -35,7 +36,9 @@ typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error); - (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name; -@property(nonatomic, strong, readonly) FSTGenericEventHandler handler; +@property(nonatomic, strong, readonly) FSTValueEventHandler valueEventHandler; +@property(nonatomic, strong, readonly) FSTErrorEventHandler errorEventHandler; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m index b44ec67..9e48ce7 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.m +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m @@ -68,8 +68,8 @@ NS_ASSUME_NONNULL_BEGIN return events[0]; } -// Overrides the handler property -- (void (^)(id _Nullable, NSError *))handler { +// Override the valueEventHandler property +- (void (^)(id _Nullable, NSError *))valueEventHandler { return ^void(id _Nullable value, NSError *error) { // We can't store nil in the _events array, but these are still interesting to tests so store // NSNull instead. @@ -82,6 +82,16 @@ NS_ASSUME_NONNULL_BEGIN }; } +// Override the errorEventHandler property +- (void (^)(NSError *))errorEventHandler { + return ^void(NSError *error) { + @synchronized(self) { + [_events addObject:[NSNull null]]; + [self checkFulfilled]; + } + }; +} + - (void)checkFulfilled { if (_events.count >= self.maxEvents) { [self.expectation fulfill]; 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<NSString *, id> *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} + +- (NSDictionary<NSString *, id> *)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<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue { +- (NSDictionary<NSString *, id> *)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<id> *)convertedArray:(FSTArrayValue *)arrayValue { +- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue + options:(FSTFieldValueOptions *)options { NSArray<FSTFieldValue *> *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 <Foundation/Foundation.h> + +@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<NSString *, id> *)value; +- (NSDictionary<NSString *, id> *)valueWithOptions:(FSTFieldValueOptions *)options; - (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)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<id> *)value; +- (NSArray<id> *)valueWithOptions:(FSTFieldValueOptions *)options; - (NSArray<FSTFieldValue *> *)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<FSTFieldValue *> *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<FSTFieldValue *> *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime { +- (NSArray<FSTFieldValue *> *) +localTransformResultsWithPreviousDocument:(FSTMaybeDocument *_Nullable)previousDocument + writeTime:(FSTTimestamp *)localWriteTime { NSMutableArray<FSTFieldValue *> *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 @@ -22,6 +22,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 * field. @@ -48,11 +91,56 @@ 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<NSString *, id> *)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<NSString *, id> *)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. * * @param key The field to retrieve. |