diff options
author | Michael Lehenbauer <mikelehen@gmail.com> | 2018-01-10 10:46:09 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-01-10 10:46:09 -0800 |
commit | 90dedd3d024a2859d0cf514efbfc97e471891c18 (patch) | |
tree | a1096470f09686afd94cbb140335c31366038fc5 | |
parent | 4dd07c6ea00d01fe1645b635fd4dfa2940b06de7 (diff) | |
parent | dc0b29e9cf7febe201a3845782655ec80d9e19f4 (diff) |
Merge pull request #639 from firebase/firestore-api-changes
Merge firestore-api-changes to master for next release.
98 files changed, 2557 insertions, 642 deletions
@@ -59,5 +59,6 @@ Podfile.lock *.xcworkspace # CMake +.downloads Debug Release diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index ae9f994..59858e6 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,18 @@ -# Unreleased +# Unreleased (firestore-api-changes) +- [changed] Removed the includeMetadataChanges property in FIRDocumentListenOptions + to avoid confusion with the factory method of the same name. +- [changed] Added a commit method that takes no completion handler to FIRWriteBatch. +- [feature] Queries can now be created from an NSPredicate. +- [added] Added SnapshotOptions API to control how DocumentSnapshots return unresolved + server timestamps. +- [changed] For non-existing documents, DocumentSnapshot.data() now returns `nil` + instead of throwing an exception. A non-nullable QueryDocumentSnapshot is + introduced for Queries to reduce the number of nil-checks in your code. +- [changed] Snapshot listeners (with the `includeMetadataChanges` option + enabled) now receive an event with `snapshot.metadata.isFromCache` set to + `true` if the SDK loses its connection to the backend. A new event with + `snapshot.metadata.isFromCache` set to false will be raised once the + connection is restored and the query is in sync with the backend again. # v0.9.4 - [changed] Firestore no longer has a direct dependency on FirebaseAuth. diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index a8ad799..06f790c 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -63,6 +63,15 @@ 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; + AB382F7C1FE02A1F007CA955 /* FIRDocumentReferenceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */; }; + AB382F7E1FE03059007CA955 /* FIRFieldPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */; }; + AB9945261FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */; }; + AB9945281FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */; }; + AB99452A1FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */; }; + AB99452C1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */; }; + AB99452E1FE30AC800DFC1E6 /* FIRFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */; }; + ABAEEF4F1FD5F8B100C966CB /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */; }; + ABF341051FE860CA00C48322 /* FSTAPIHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = ABF341021FE8593500C48322 /* FSTAPIHelpers.m */; }; AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; D5B2532E4676014F57A7EAB9 /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B25C0D4AADFCA3ADB883E4 /* FSTStreamTests.m */; }; @@ -234,6 +243,16 @@ 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = "<group>"; }; 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; }; 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = "<group>"; }; + AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRDocumentReferenceTests.m; sourceTree = "<group>"; }; + AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRFieldPathTests.m; sourceTree = "<group>"; }; + AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCollectionReferenceTests.m; sourceTree = "<group>"; }; + AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRSnapshotMetadataTests.m; sourceTree = "<group>"; }; + AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDocumentSnapshotTests.m; sourceTree = "<group>"; }; + AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQuerySnapshotTests.m; sourceTree = "<group>"; }; + AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldValueTests.m; sourceTree = "<group>"; }; + ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = "<group>"; }; + ABF341011FE858B500C48322 /* FSTAPIHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTAPIHelpers.h; sourceTree = "<group>"; }; + ABF341021FE8593500C48322 /* FSTAPIHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAPIHelpers.m; sourceTree = "<group>"; }; B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; @@ -589,7 +608,17 @@ DE51B1831F0D48AC0013853F /* API */ = { isa = PBXGroup; children = ( + AB99452D1FE30AC800DFC1E6 /* FIRFieldValueTests.m */, + AB99452B1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m */, + AB9945291FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m */, + AB9945271FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m */, + AB9945251FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m */, + AB382F7D1FE03059007CA955 /* FIRFieldPathTests.m */, + AB382F7B1FE02A1F007CA955 /* FIRDocumentReferenceTests.m */, DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */, + ABAEEF4E1FD5F8B100C966CB /* FIRQueryTests.m */, + ABF341011FE858B500C48322 /* FSTAPIHelpers.h */, + ABF341021FE8593500C48322 /* FSTAPIHelpers.m */, ); path = API; sourceTree = "<group>"; @@ -923,8 +952,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", @@ -936,7 +964,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", @@ -1167,7 +1194,9 @@ files = ( DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */, + ABAEEF4F1FD5F8B100C966CB /* FIRQueryTests.m in Sources */, DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */, + ABF341051FE860CA00C48322 /* FSTAPIHelpers.m in Sources */, DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */, DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */, DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */, @@ -1179,6 +1208,7 @@ DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */, DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */, DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */, + AB9945281FE2DE0C00DFC1E6 /* FIRSnapshotMetadataTests.m in Sources */, 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */, @@ -1192,6 +1222,7 @@ 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */, DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */, DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */, + AB99452A1FE2F9EB00DFC1E6 /* FIRDocumentSnapshotTests.m in Sources */, DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */, DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */, DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, @@ -1203,6 +1234,7 @@ DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */, 54764FAB1FAA0C320085E60A /* string_util_test.cc in Sources */, 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */, + AB99452E1FE30AC800DFC1E6 /* FIRFieldValueTests.m in Sources */, DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */, DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */, DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */, @@ -1215,7 +1247,9 @@ DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */, DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */, DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */, + AB9945261FE2D71100DFC1E6 /* FIRCollectionReferenceTests.m in Sources */, DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */, + AB382F7C1FE02A1F007CA955 /* FIRDocumentReferenceTests.m in Sources */, DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */, DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */, DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */, @@ -1224,7 +1258,9 @@ DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */, DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */, DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */, + AB382F7E1FE03059007CA955 /* FIRFieldPathTests.m in Sources */, DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */, + AB99452C1FE3018D00DFC1E6 /* FIRQuerySnapshotTests.m in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */, D5B25474286C9800CE42B8C2 /* FSTTestDispatchQueue.m in Sources */, diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift index bea8b56..260735b 100644 --- a/Firestore/Example/SwiftBuildTest/main.swift +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -27,6 +27,8 @@ func main() { writeDocument(at: documentRef); + writeDocuments(at: documentRef, database: db); + addDocument(to: collectionRef); readDocument(at: documentRef); @@ -37,6 +39,8 @@ func main() { listenToDocuments(matching: query); + enableDisableNetwork(database: db); + types(); } @@ -129,6 +133,47 @@ func writeDocument(at docRef: DocumentReference) { } } +func enableDisableNetwork(database db: Firestore) { + // closure syntax + db.disableNetwork(completion: { (error) in + if let e = error { + print("Uh oh! \(e)") + return + } + }) + // trailing block syntax + db.enableNetwork { (error) in + if let e = error { + print("Uh oh! \(e)") + return + } + } +} + +func writeDocuments(at docRef: DocumentReference, database db: Firestore) { + var batch: WriteBatch; + + batch = db.batch(); + batch.setData(["a" : "b"], forDocument:docRef); + batch.setData(["c" : "d"], forDocument:docRef); + // commit without completion callback. + batch.commit(); + print("Batch write without completion complete!"); + + batch = db.batch(); + batch.setData(["a" : "b"], forDocument:docRef); + batch.setData(["c" : "d"], forDocument:docRef); + // commit with completion callback via trailing closure syntax. + batch.commit() { error in + if let error = error { + print("Uh oh! \(error)"); + return; + } + print("Batch write callback complete!"); + } + print("Batch write with completion complete!"); +} + func addDocument(to collectionRef: CollectionReference) { collectionRef.addDocument(data: ["foo": 42]); @@ -141,11 +186,20 @@ func readDocument(at docRef: DocumentReference) { // Trailing closure syntax. docRef.getDocument() { document, error in if let document = document { - // NOTE that document is nullable. - let data = document.data(); + // Note that both document and document.data() is nullable. + if let data = document.data() { print("Read document: \(data)") - - // Fields are read via subscript notation. + } + if let data = document.data(with:SnapshotOptions.serverTimestampBehavior(.estimate)) { + print("Read document: \(data)") + } + if let foo = document.get("foo") { + print("Field: \(foo)") + } + if let foo = document.get("foo", options: SnapshotOptions.serverTimestampBehavior(.previous)) { + print("Field: \(foo)") + } + // Fields can also be read via subscript notation. if let foo = document["foo"] { print("Field: \(foo)") } @@ -181,24 +235,27 @@ func readDocuments(matching query: Query) { func listenToDocument(at docRef: DocumentReference) { - let listener = docRef.addSnapshotListener() { document, error in - if let error = error { - print("Uh oh! Listen canceled: \(error)") - return - } + let listener = docRef.addSnapshotListener() { document, error in + if let error = error { + print("Uh oh! Listen canceled: \(error)") + return + } - if let document = document { - print("Current document: \(document.data())"); - if (document.metadata.isFromCache) { - print("From Cache") - } else { - print("From Server") - } - } + if let document = document { + // Note that document.data() is nullable. + if let data : [String:Any] = document.data() { + print("Current document: \(data)"); + } + if document.metadata.isFromCache { + print("From Cache") + } else { + print("From Server") + } } + } - // Unsubscribe. - listener.remove(); + // Unsubscribe. + listener.remove(); } func listenToDocuments(matching query: Query) { @@ -215,7 +272,9 @@ func listenToDocuments(matching query: Query) { // TODO(mikelehen): Figure out how to make "for..in" syntax work // directly on documentSet. for document in snap.documents { - print("Doc: ", document.data()) + // Note that document.data() is not nullable. + let data : [String:Any] = document.data() + print("Doc: ", data) } } } @@ -258,7 +317,7 @@ func transactions() { let balanceA = try transaction.getDocument(accA)["balance"] as! Double let balanceB = try transaction.getDocument(accB)["balance"] as! Double - if (balanceA < amount) { + if balanceA < amount { errorPointer?.pointee = NSError(domain: "Foo", code: 123, userInfo: nil) return nil } diff --git a/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m b/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m new file mode 100644 index 0000000..73ae38d --- /dev/null +++ b/Firestore/Example/Tests/API/FIRCollectionReferenceTests.m @@ -0,0 +1,43 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRCollectionReference.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRCollectionReferenceTests : XCTestCase +@end + +@implementation FIRCollectionReferenceTests + +- (void)testEquals { + FIRCollectionReference *referenceFoo = FSTTestCollectionRef(@"foo"); + FIRCollectionReference *referenceFooDup = FSTTestCollectionRef(@"foo"); + FIRCollectionReference *referenceBar = FSTTestCollectionRef(@"bar"); + XCTAssertEqualObjects(referenceFoo, referenceFooDup); + XCTAssertNotEqualObjects(referenceFoo, referenceBar); + + XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); + XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m b/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m new file mode 100644 index 0000000..4e301d0 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRDocumentReferenceTests.m @@ -0,0 +1,43 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRDocumentReference.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentReferenceTests : XCTestCase +@end + +@implementation FIRDocumentReferenceTests + +- (void)testEquals { + FIRDocumentReference *referenceFoo = FSTTestDocRef(@"rooms/foo"); + FIRDocumentReference *referenceFooDup = FSTTestDocRef(@"rooms/foo"); + FIRDocumentReference *referenceBar = FSTTestDocRef(@"rooms/bar"); + XCTAssertEqualObjects(referenceFoo, referenceFooDup); + XCTAssertNotEqualObjects(referenceFoo, referenceBar); + + XCTAssertEqual([referenceFoo hash], [referenceFooDup hash]); + XCTAssertNotEqual([referenceFoo hash], [referenceBar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m b/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m new file mode 100644 index 0000000..e865928 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRDocumentSnapshotTests.m @@ -0,0 +1,62 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRDocumentSnapshot.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshotTests : XCTestCase +@end + +@implementation FIRDocumentSnapshotTests + +- (void)testEquals { + FIRDocumentSnapshot *base = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); + FIRDocumentSnapshot *baseDup = FSTTestDocSnapshot(@"rooms/foo", 1, @{ @"a" : @1 }, NO, NO); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + FIRDocumentSnapshot *nilData = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); + FIRDocumentSnapshot *nilDataDup = FSTTestDocSnapshot(@"rooms/foo", 1, nil, NO, NO); +#pragma clang diagnostic pop + FIRDocumentSnapshot *differentPath = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, NO); + FIRDocumentSnapshot *differentData = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"b" : @1 }, NO, NO); + FIRDocumentSnapshot *hasMutations = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, YES, NO); + FIRDocumentSnapshot *fromCache = FSTTestDocSnapshot(@"rooms/bar", 1, @{ @"a" : @1 }, NO, YES); + XCTAssertEqualObjects(base, baseDup); + XCTAssertEqualObjects(nilData, nilDataDup); + XCTAssertNotEqualObjects(base, nilData); + XCTAssertNotEqualObjects(nilData, base); + XCTAssertNotEqualObjects(base, differentPath); + XCTAssertNotEqualObjects(base, differentData); + XCTAssertNotEqualObjects(base, hasMutations); + XCTAssertNotEqualObjects(base, fromCache); + + XCTAssertEqual([base hash], [baseDup hash]); + XCTAssertEqual([nilData hash], [nilDataDup hash]); + XCTAssertNotEqual([base hash], [nilData hash]); + XCTAssertNotEqual([base hash], [differentPath hash]); + XCTAssertNotEqual([base hash], [differentData hash]); + XCTAssertNotEqual([base hash], [hasMutations hash]); + XCTAssertNotEqual([base hash], [fromCache hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldPathTests.m b/Firestore/Example/Tests/API/FIRFieldPathTests.m new file mode 100644 index 0000000..f8177c8 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRFieldPathTests.m @@ -0,0 +1,45 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRFieldPath.h" +#import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldPathTests : XCTestCase +@end + +@implementation FIRFieldPathTests + +- (void)testEquals { + FIRFieldPath *foo = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; + FIRFieldPath *fooDup = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"foo.ooo.oooo")]; + FIRFieldPath *bar = [[FIRFieldPath alloc] initPrivate:FSTTestFieldPath(@"baa.aaa.aaar")]; + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, bar); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [bar hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRFieldValueTests.m b/Firestore/Example/Tests/API/FIRFieldValueTests.m new file mode 100644 index 0000000..8c9db99 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRFieldValueTests.m @@ -0,0 +1,48 @@ +/* + * 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 FirebaseFirestore; + +#import <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRFieldValue.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValueTests : XCTestCase +@end + +@implementation FIRFieldValueTests + +- (void)testEquals { + FIRFieldValue *delete = [FIRFieldValue fieldValueForDelete]; + FIRFieldValue *deleteDup = [FIRFieldValue fieldValueForDelete]; + FIRFieldValue *serverTimestamp = [FIRFieldValue fieldValueForServerTimestamp]; + FIRFieldValue *serverTimestampDup = [FIRFieldValue fieldValueForServerTimestamp]; + XCTAssertEqualObjects(delete, deleteDup); + XCTAssertNotEqualObjects(delete, nil); + XCTAssertEqualObjects(serverTimestamp, serverTimestampDup); + XCTAssertNotEqualObjects(serverTimestamp, nil); + XCTAssertNotEqualObjects(delete, serverTimestamp); + + XCTAssertEqual([delete hash], [deleteDup hash]); + XCTAssertEqual([serverTimestamp hash], [serverTimestamp hash]); + XCTAssertNotEqual([delete hash], [serverTimestamp hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m index b505de0..8abda10 100644 --- a/Firestore/Example/Tests/API/FIRGeoPointTests.m +++ b/Firestore/Example/Tests/API/FIRGeoPointTests.m @@ -28,16 +28,17 @@ NS_ASSUME_NONNULL_BEGIN @implementation FIRGeoPointTests - (void)testEquals { - XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], - [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0]); - XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56], - [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]); - XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], - [[FIRGeoPoint alloc] initWithLatitude:1 longitude:0]); - XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], - [[FIRGeoPoint alloc] initWithLatitude:0 longitude:1]); - XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0], - [[NSObject alloc] init]); + FIRGeoPoint *foo = FSTTestGeoPoint(1.23, 4.56); + FIRGeoPoint *fooDup = FSTTestGeoPoint(1.23, 4.56); + FIRGeoPoint *differentLatitude = FSTTestGeoPoint(1.23, 0); + FIRGeoPoint *differentLongitude = FSTTestGeoPoint(0, 4.56); + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, differentLatitude); + XCTAssertNotEqualObjects(foo, differentLongitude); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [differentLatitude hash]); + XCTAssertNotEqual([foo hash], [differentLongitude hash]); } - (void)testComparison { diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m new file mode 100644 index 0000000..4637c49 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.m @@ -0,0 +1,57 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRQuerySnapshot.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshotTests : XCTestCase +@end + +@implementation FIRQuerySnapshotTests + +- (void)testEquals { + FIRQuerySnapshot *foo = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *fooDup = FSTTestQuerySnapshot(@"foo", @{}, @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *differentPath = FSTTestQuerySnapshot(@"bar", @{}, + @{ @"a" : @{@"a" : @1} }, YES, NO); + FIRQuerySnapshot *differentDoc = FSTTestQuerySnapshot(@"foo", + @{ @"a" : @{@"b" : @1} }, @{}, YES, NO); + FIRQuerySnapshot *noPendingWrites = FSTTestQuerySnapshot(@"foo", @{}, + @{ @"a" : @{@"a" : @1} }, NO, NO); + FIRQuerySnapshot *fromCache = FSTTestQuerySnapshot(@"foo", @{}, + @{ @"a" : @{@"a" : @1} }, YES, YES); + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, differentPath); + XCTAssertNotEqualObjects(foo, differentDoc); + XCTAssertNotEqualObjects(foo, noPendingWrites); + XCTAssertNotEqualObjects(foo, fromCache); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [differentPath hash]); + XCTAssertNotEqual([foo hash], [differentDoc hash]); + XCTAssertNotEqual([foo hash], [noPendingWrites hash]); + XCTAssertNotEqual([foo hash], [fromCache hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRQueryTests.m b/Firestore/Example/Tests/API/FIRQueryTests.m new file mode 100644 index 0000000..1b5236d --- /dev/null +++ b/Firestore/Example/Tests/API/FIRQueryTests.m @@ -0,0 +1,84 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRQuery.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTPath.h" + +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryTests : XCTestCase +@end + +@implementation FIRQueryTests + +- (void)testEquals { + FIRFirestore *firestore = FSTTestFirestore(); + FIRQuery *queryFoo = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *queryFooDup = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *queryBar = [FIRQuery referenceWithQuery:FSTTestQuery(@"bar") firestore:firestore]; + XCTAssertEqualObjects(queryFoo, queryFooDup); + XCTAssertNotEqualObjects(queryFoo, queryBar); + XCTAssertEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], + [queryFoo queryWhereField:@"f" isEqualTo:@1]); + XCTAssertNotEqualObjects([queryFoo queryWhereField:@"f" isEqualTo:@1], + [queryFoo queryWhereField:@"f" isEqualTo:@2]); + + XCTAssertEqual([queryFoo hash], [queryFooDup hash]); + XCTAssertNotEqual([queryFoo hash], [queryBar hash]); + XCTAssertEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], + [[queryFoo queryWhereField:@"f" isEqualTo:@1] hash]); + XCTAssertNotEqual([[queryFoo queryWhereField:@"f" isEqualTo:@1] hash], + [[queryFoo queryWhereField:@"f" isEqualTo:@2] hash]); +} + +- (void)testFilteringWithPredicate { + FIRFirestore *firestore = FSTTestFirestore(); + FIRQuery *query = [FIRQuery referenceWithQuery:FSTTestQuery(@"foo") firestore:firestore]; + FIRQuery *query1 = [query queryWhereField:@"f" isLessThanOrEqualTo:@1]; + FIRQuery *query2 = [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f<=1"]]; + FIRQuery *query3 = + [[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3]; + FIRQuery *query4 = + [query queryFilteredUsingPredicate:[NSPredicate predicateWithFormat:@"f1<2 && f2==3"]]; + FIRQuery *query5 = + [[[[[query queryWhereField:@"f1" isLessThan:@2] queryWhereField:@"f2" isEqualTo:@3] + queryWhereField:@"f1" + isLessThanOrEqualTo:@"four"] queryWhereField:@"f1" + isGreaterThanOrEqualTo:@"five"] queryWhereField:@"f1" + isGreaterThan:@6]; + FIRQuery *query6 = [query + queryFilteredUsingPredicate: + [NSPredicate predicateWithFormat:@"f1<2 && f2==3 && f1<='four' && f1>='five' && f1>6"]]; + FIRQuery *query7 = [query + queryFilteredUsingPredicate: + [NSPredicate predicateWithFormat:@"2>f1 && 3==f2 && 'four'>=f1 && 'five'<=f1 && 6<f1"]]; + XCTAssertEqualObjects(query1, query2); + XCTAssertNotEqualObjects(query2, query3); + XCTAssertEqualObjects(query3, query4); + XCTAssertNotEqualObjects(query4, query5); + XCTAssertEqualObjects(query5, query6); + XCTAssertEqualObjects(query6, query7); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.m b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.m new file mode 100644 index 0000000..cf50765 --- /dev/null +++ b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.m @@ -0,0 +1,51 @@ +/* + * 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 <XCTest/XCTest.h> + +#import "FirebaseFirestore/FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadataTests : XCTestCase +@end + +@implementation FIRSnapshotMetadataTests + +- (void)testEquals { + FIRSnapshotMetadata *foo = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; + FIRSnapshotMetadata *fooDup = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:YES]; + FIRSnapshotMetadata *bar = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES fromCache:NO]; + FIRSnapshotMetadata *baz = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO fromCache:YES]; + XCTAssertEqualObjects(foo, fooDup); + XCTAssertNotEqualObjects(foo, bar); + XCTAssertNotEqualObjects(foo, baz); + XCTAssertNotEqualObjects(bar, baz); + + XCTAssertEqual([foo hash], [fooDup hash]); + XCTAssertNotEqual([foo hash], [bar hash]); + XCTAssertNotEqual([foo hash], [baz hash]); + XCTAssertNotEqual([bar hash], [baz hash]); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.h b/Firestore/Example/Tests/API/FSTAPIHelpers.h new file mode 100644 index 0000000..dcd8209 --- /dev/null +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.h @@ -0,0 +1,73 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FirebaseFirestore/FIRCollectionReference.h" +#import "FirebaseFirestore/FIRDocumentSnapshot.h" +#import "FirebaseFirestore/FIRFirestore.h" +#import "FirebaseFirestore/FIRQuerySnapshot.h" + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" + +NS_ASSUME_NONNULL_BEGIN + +#if __cplusplus +extern "C" { +#endif + +/** A convenience method for creating dummy singleton FIRFirestore for tests. */ +FIRFirestore *FSTTestFirestore(); + +/** A convenience method for creating a doc snapshot for tests. */ +FIRDocumentSnapshot *FSTTestDocSnapshot(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary<NSString *, id> *data, + BOOL hasMutations, + BOOL fromCache); + +/** A convenience method for creating a collection reference from a path string. */ +FIRCollectionReference *FSTTestCollectionRef(NSString *path); + +/** A convenience method for creating a document reference from a path string. */ +FIRDocumentReference *FSTTestDocRef(NSString *path); + +/** + * A convenience method for creating a particular query snapshot for tests. + * + * @param path To be used in constructing the query. + * @param oldDocs Provides the prior set of documents in the QuerySnapshot. Each dictionary entry + * maps to a document, with the key being the document id, and the value being the document + * contents. + * @param docsToAdd Specifies data to be added into the query snapshot as of now. Each dictionary + * entry maps to a document, with the key being the document id, and the value being the document + * contents. + * @param hasPendingWrites Whether the query snapshot has pending writes to the server. + * @param fromCache Whether the query snapshot is cache result. + * @returns A query snapshot that consists of both sets of documents. + */ +FIRQuerySnapshot *FSTTestQuerySnapshot( + NSString *path, + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *oldDocs, + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *docsToAdd, + BOOL hasPendingWrites, + BOOL fromCache); + +#if __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.m b/Firestore/Example/Tests/API/FSTAPIHelpers.m new file mode 100644 index 0000000..507e2ff --- /dev/null +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.m @@ -0,0 +1,113 @@ +/* + * 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 "Firestore/Example/Tests/API/FSTAPIHelpers.h" + +#import "FirebaseFirestore/FIRDocumentReference.h" +#import "FirebaseFirestore/FIRSnapshotMetadata.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTDocumentKey.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Model/FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +FIRFirestore *FSTTestFirestore() { + static FIRFirestore *sharedInstance = nil; + static dispatch_once_t onceToken; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRFirestore alloc] initWithProjectID:@"abc" + database:@"abc" + persistenceKey:@"db123" + credentialsProvider:nil + workerDispatchQueue:nil + firebaseApp:nil]; + }); +#pragma clang diagnostic pop + return sharedInstance; +} + +FIRDocumentSnapshot *FSTTestDocSnapshot(NSString *path, + FSTTestSnapshotVersion version, + NSDictionary<NSString *, id> *data, + BOOL hasMutations, + BOOL fromCache) { + FSTDocument *doc = data ? FSTTestDoc(path, version, data, hasMutations) : nil; + return [FIRDocumentSnapshot snapshotWithFirestore:FSTTestFirestore() + documentKey:FSTTestDocKey(path) + document:doc + fromCache:fromCache]; +} + +FIRCollectionReference *FSTTestCollectionRef(NSString *path) { + return [FIRCollectionReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; +} + +FIRDocumentReference *FSTTestDocRef(NSString *path) { + return [FIRDocumentReference referenceWithPath:FSTTestPath(path) firestore:FSTTestFirestore()]; +} + +/** A convenience method for creating a query snapshots for tests. */ +FIRQuerySnapshot *FSTTestQuerySnapshot( + NSString *path, + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *oldDocs, + NSDictionary<NSString *, NSDictionary<NSString *, id> *> *docsToAdd, + BOOL hasPendingWrites, + BOOL fromCache) { + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:hasPendingWrites fromCache:fromCache]; + FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[]); + for (NSString *key in oldDocs) { + oldDocuments = [oldDocuments + documentSetByAddingDocument:FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, + oldDocs[key], hasPendingWrites)]; + } + FSTDocumentSet *newDocuments = oldDocuments; + NSArray<FSTDocumentViewChange *> *documentChanges = [NSArray array]; + for (NSString *key in docsToAdd) { + FSTDocument *docToAdd = FSTTestDoc([NSString stringWithFormat:@"%@/%@", path, key], 1, + docsToAdd[key], hasPendingWrites); + newDocuments = [newDocuments documentSetByAddingDocument:docToAdd]; + documentChanges = [documentChanges + arrayByAddingObject:[FSTDocumentViewChange + changeWithDocument:docToAdd + type:FSTDocumentViewChangeTypeAdded]]; + } + FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:FSTTestQuery(path) + documents:newDocuments + oldDocuments:oldDocuments + documentChanges:documentChanges + fromCache:fromCache + hasPendingWrites:hasPendingWrites + syncStateChanged:YES]; + return [FIRQuerySnapshot snapshotWithFirestore:FSTTestFirestore() + originalQuery:FSTTestQuery(path) + snapshot:viewSnapshot + metadata:metadata]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m index 99021ce..fcde17d 100644 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.m +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.m @@ -52,7 +52,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testHandlesManyListenersPerQuery { - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); FSTQueryListener *listener1 = [self noopListenerForQuery:query]; FSTQueryListener *listener2 = [self noopListenerForQuery:query]; @@ -73,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testHandlesUnlistenOnUnknownListenerGracefully { - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); FSTQueryListener *listener = [self noopListenerForQuery:query]; FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); @@ -95,8 +95,8 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testNotifiesListenersInTheRightOrder { - FSTQuery *query1 = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; - FSTQuery *query2 = [FSTQuery queryWithPath:FSTTestPath(@"bar/baz")]; + FSTQuery *query1 = FSTTestQuery(@"foo/bar"); + FSTQuery *query2 = FSTTestQuery(@"bar/baz"); NSMutableArray *eventOrder = [NSMutableArray array]; FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 @@ -135,15 +135,15 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testWillForwardOnlineStateChanges { - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]); NSMutableArray *events = [NSMutableArray array]; OCMStub([fakeListener query]).andReturn(query); - OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateUnknown]) + OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateUnknown]) .andDo(^(NSInvocation *invocation) { [events addObject:@(FSTOnlineStateUnknown)]; }); - OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateHealthy]) + OCMStub([fakeListener applyChangedOnlineState:FSTOnlineStateHealthy]) .andDo(^(NSInvocation *invocation) { [events addObject:@(FSTOnlineStateHealthy)]; }); @@ -154,7 +154,7 @@ NS_ASSUME_NONNULL_BEGIN [eventManager addListener:fakeListener]; XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]); - [eventManager watchStreamDidChangeOnlineState:FSTOnlineStateHealthy]; + [eventManager applyChangedOnlineState:FSTOnlineStateHealthy]; XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ])); } diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m index 1bb7a47..4856b5f 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m @@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; NSMutableArray<FSTViewSnapshot *> *otherAccum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); FSTDocument *doc2prime = @@ -88,7 +88,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRaisesErrorEvent { NSMutableArray<NSError *> *accum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + FSTQuery *query = FSTTestQuery(@"rooms/Eros"); FSTQueryListener *listener = [self listenToQuery:query handler:^(FSTViewSnapshot *snapshot, NSError *error) { @@ -104,7 +104,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRaisesEventForEmptyCollectionAfterSync { NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; @@ -126,7 +126,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testMutingAsyncListenerPreventsAllSubsequentEvents { NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")]; + FSTQuery *query = FSTTestQuery(@"rooms/Eros"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); @@ -166,7 +166,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); @@ -204,7 +204,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); @@ -253,7 +253,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES); FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); @@ -290,7 +290,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); @@ -322,7 +322,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testWillWaitForSyncIfOnline { NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); FSTQueryListener *listener = @@ -340,10 +340,10 @@ NS_ASSUME_NONNULL_BEGIN [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); - [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event [listener queryDidChangeViewSnapshot:snap1]; - [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; - [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; + [listener applyChangedOnlineState:FSTOnlineStateUnknown]; + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; [listener queryDidChangeViewSnapshot:snap2]; [listener queryDidChangeViewSnapshot:snap3]; @@ -365,7 +365,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testWillRaiseInitialEventWhenGoingOffline { NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO); FSTQueryListener *listener = @@ -379,12 +379,12 @@ NS_ASSUME_NONNULL_BEGIN FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event - [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; // no event - [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event - [listener queryDidChangeViewSnapshot:snap2]; // another event + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event + [listener applyChangedOnlineState:FSTOnlineStateUnknown]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap2]; // another event FSTDocumentViewChange *change1 = [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded]; @@ -411,7 +411,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTQueryListener *listener = [self listenToQuery:query options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; @@ -419,9 +419,9 @@ NS_ASSUME_NONNULL_BEGIN FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // no event - [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event + [listener applyChangedOnlineState:FSTOnlineStateHealthy]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // no event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // event FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] initWithQuery:query @@ -437,7 +437,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")]; + FSTQuery *query = FSTTestQuery(@"rooms"); FSTQueryListener *listener = [self listenToQuery:query options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; @@ -445,8 +445,8 @@ NS_ASSUME_NONNULL_BEGIN FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event - [listener queryDidChangeViewSnapshot:snap1]; // event + [listener applyChangedOnlineState:FSTOnlineStateFailed]; // no event + [listener queryDidChangeViewSnapshot:snap1]; // event FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] initWithQuery:query diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m index 1fd0e8b..3d2bd82 100644 --- a/Firestore/Example/Tests/Core/FSTQueryTests.m +++ b/Firestore/Example/Tests/Core/FSTQueryTests.m @@ -61,9 +61,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testOrderBy { - FSTResourcePath *path = - [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages" ]]; - FSTQuery *query = [FSTQuery queryWithPath:path]; + FSTQuery *query = FSTTestQuery(@"rooms/Firestore/messages"); query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length") ascending:NO]]; @@ -80,29 +78,25 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testMatchesBasedOnDocumentKey { - FSTResourcePath *queryKey = - [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages", @"1" ]]; FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); // document query - FSTQuery *query = [FSTQuery queryWithPath:queryKey]; + FSTQuery *query = FSTTestQuery(@"rooms/eros/messages/1"); XCTAssertTrue([query matchesDocument:doc1]); XCTAssertFalse([query matchesDocument:doc2]); XCTAssertFalse([query matchesDocument:doc3]); } - (void)testMatchesCorrectlyForShallowAncestorQuery { - FSTResourcePath *queryPath = - [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]; FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); // shallow ancestor query - FSTQuery *query = [FSTQuery queryWithPath:queryPath]; + FSTQuery *query = FSTTestQuery(@"rooms/eros/messages"); XCTAssertTrue([query matchesDocument:doc1]); XCTAssertFalse([query matchesDocument:doc1Meta]); XCTAssertTrue([query matchesDocument:doc2]); @@ -110,21 +104,20 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEmptyFieldsAreAllowedForQueries { - FSTResourcePath *queryPath = [FSTResourcePath pathWithString:@"rooms/eros/messages"]; FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO); - FSTQuery *query = [[FSTQuery queryWithPath:queryPath] + FSTQuery *query = [FSTTestQuery(@"rooms/eros/messages") queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")]; XCTAssertTrue([query matchesDocument:doc1]); XCTAssertFalse([query matchesDocument:doc2]); } - (void)testMatchesPrimitiveValuesForFilters { - FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] - queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; - FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] - queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + FSTQuery *query1 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + FSTQuery *query2 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); @@ -149,7 +142,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testNullFilter { - FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + FSTQuery *query = [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])]; FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO); FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); @@ -165,8 +158,8 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testNanFilter { - FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] - queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; + FSTQuery *query = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))]; FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO); FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO); FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO); @@ -181,10 +174,10 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testDoesNotMatchComplexObjectsForFilters { - FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] - queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; - FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] - queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; + FSTQuery *query1 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))]; + FSTQuery *query2 = + [FSTTestQuery(@"collection") queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))]; FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO); FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO); @@ -212,7 +205,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testDoesntRemoveComplexObjectsWithOrderBy { - FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]] + FSTQuery *query1 = [FSTTestQuery(@"collection") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") ascending:YES]]; @@ -232,9 +225,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testFiltersBasedOnArrayValue { - FSTQuery *baseQuery = - [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; - + FSTQuery *baseQuery = FSTTestQuery(@"collection"); FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO); NSArray<id<FSTFilter>> *matchingFilters = @@ -256,9 +247,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testFiltersBasedOnObjectValue { - FSTQuery *baseQuery = - [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; - + FSTQuery *baseQuery = FSTTestQuery(@"collection"); FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO); @@ -310,7 +299,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testSortsDocumentsInTheCorrectOrder { - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + FSTQuery *query = FSTTestQuery(@"collection"); query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort") ascending:YES]]; @@ -339,7 +328,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testSortsDocumentsUsingMultipleFields { - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + FSTQuery *query = FSTTestQuery(@"collection"); query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") ascending:YES]]; @@ -366,7 +355,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testSortsDocumentsWithDescendingToo { - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]; + FSTQuery *query = FSTTestQuery(@"collection"); query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1") ascending:NO]]; @@ -393,40 +382,40 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEquality { - FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q11 = FSTTestQuery(@"foo"); q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q12 = FSTTestQuery(@"foo"); q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; - FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q21 = FSTTestQuery(@"foo"); + FSTQuery *q22 = FSTTestQuery(@"foo"); - FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; - FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q31 = FSTTestQuery(@"foo/bar"); + FSTQuery *q32 = FSTTestQuery(@"foo/bar"); - FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q41 = FSTTestQuery(@"foo"); q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q42 = FSTTestQuery(@"foo"); q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q43Diff = FSTTestQuery(@"foo"); q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q51 = FSTTestQuery(@"foo"); q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q52 = FSTTestQuery(@"foo"); q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q53Diff = FSTTestQuery(@"foo"); q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q61 = FSTTestQuery(@"foo"); q61 = [q61 queryBySettingLimit:10]; // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet @@ -458,40 +447,40 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testUniqueIds { - FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q11 = FSTTestQuery(@"foo"); q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; - FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q12 = FSTTestQuery(@"foo"); q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))]; q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))]; - FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; - FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q21 = FSTTestQuery(@"foo"); + FSTQuery *q22 = FSTTestQuery(@"foo"); - FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; - FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *q31 = FSTTestQuery(@"foo/bar"); + FSTQuery *q32 = FSTTestQuery(@"foo/bar"); - FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q41 = FSTTestQuery(@"foo"); q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES]; q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q42 = FSTTestQuery(@"foo"); q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES]; q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q43Diff = FSTTestQuery(@"foo"); q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES]; q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q51 = FSTTestQuery(@"foo"); q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES]; q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; - FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q52 = FSTTestQuery(@"foo"); q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))]; q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES]; - FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q53Diff = FSTTestQuery(@"foo"); q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))]; q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES]; - FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *q61 = FSTTestQuery(@"foo"); q61 = [q61 queryBySettingLimit:10]; // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m index 5d3787a..fe3e42d 100644 --- a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m @@ -107,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testViewSnapshotConstructor { - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"a" ]]]; + FSTQuery *query = FSTTestQuery(@"a"); FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; FSTDocumentSet *oldDocuments = documents; documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)]; diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m index 087eb01..f557ee6 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m @@ -16,6 +16,7 @@ @import FirebaseFirestore; +#import <FirebaseFirestore/FIRFirestore.h> #import <XCTest/XCTest.h> #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @@ -83,6 +84,13 @@ XCTAssertFalse(result.exists); } +- (void)testCanRetrieveDocumentThatDoesNotExist { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; + XCTAssertNil(result.data); + XCTAssertNil(result[@"foo"]); +} + - (void)testCannotUpdateNonexistentDocument { FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; @@ -844,7 +852,7 @@ FIRFirestore *firestore = doc.firestore; NSDictionary<NSString *, id> *data = @{@"a" : @"b"}; - [firestore.client disableNetworkWithCompletion:^(NSError *error) { + [firestore disableNetworkWithCompletion:^(NSError *error) { XCTAssertNil(error); [doc setData:data @@ -853,7 +861,7 @@ [writeEpectation fulfill]; }]; - [firestore.client enableNetworkWithCompletion:^(NSError *error) { + [firestore enableNetworkWithCompletion:^(NSError *error) { XCTAssertNil(error); [networkExpectation fulfill]; }]; @@ -883,7 +891,7 @@ __weak FIRDocumentReference *weakDoc = doc; - [firestore.client disableNetworkWithCompletion:^(NSError *error) { + [firestore disableNetworkWithCompletion:^(NSError *error) { XCTAssertNil(error); [doc setData:data completion:^(NSError *_Nullable error) { @@ -904,7 +912,7 @@ // Verify that we are reading from cache. XCTAssertTrue(snapshot.metadata.fromCache); XCTAssertEqualObjects(snapshot.data, data); - [firestore.client enableNetworkWithCompletion:^(NSError *error) { + [firestore enableNetworkWithCompletion:^(NSError *error) { [networkExpectation fulfill]; }]; }]; @@ -931,4 +939,25 @@ [self readSnapshotForRef:[self documentRef] requireOnline:YES]; } +- (void)testCanDisableNetwork { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + + [firestore enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; + [self awaitExpectations]; + [firestore + enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network again"]]; + [self awaitExpectations]; + [firestore + disableNetworkWithCompletion:[self completionForExpectationWithName:@"Disable network"]]; + [self awaitExpectations]; + [firestore + disableNetworkWithCompletion:[self + completionForExpectationWithName:@"Disable network again"]]; + [self awaitExpectations]; + [firestore + enableNetworkWithCompletion:[self completionForExpectationWithName:@"Final enable network"]]; + [self awaitExpectations]; +} + @end diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m index 9751844..52d73b1 100644 --- a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m @@ -128,35 +128,4 @@ [two remove]; } -- (void)testWatchSurvivesNetworkDisconnect { - XCTestExpectation *testExpectiation = - [self expectationWithDescription:@"testWatchSurvivesNetworkDisconnect"]; - - FIRCollectionReference *collectionRef = [self collectionRef]; - FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; - - FIRFirestore *firestore = collectionRef.firestore; - - FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] - includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; - - [collectionRef addSnapshotListenerWithOptions:options - listener:^(FIRQuerySnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - if (!snapshot.empty && !snapshot.metadata.fromCache) { - [testExpectiation fulfill]; - } - }]; - - [firestore.client disableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - [docRef setData:@{@"foo" : @"bar"}]; - [firestore.client enableNetworkWithCompletion:^(NSError *error) { - XCTAssertNil(error); - }]; - }]; - - [self awaitExpectations]; -} - @end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m index 180b423..831c897 100644 --- a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m @@ -18,9 +18,10 @@ #import <XCTest/XCTest.h> -#import "Firestore/Source/Core/FSTFirestoreClient.h" - +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" @interface FIRQueryTests : FSTIntegrationTestCase @end @@ -111,6 +112,23 @@ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); } +- (void)testQueryWithPredicate { + FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ + @"a" : @{@"a" : @1}, + @"b" : @{@"a" : @2}, + @"c" : @{@"a" : @3} + }]; + + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"a < 3"]; + FIRQuery *query = [collRef queryFilteredUsingPredicate:predicate]; + query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] + descending:YES]; + + FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query]; + + XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ])); +} + - (void)testFilterOnInfinity { FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{ @"a" : @{@"inf" : @(INFINITY)}, @@ -194,4 +212,67 @@ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ])); } +- (void)testWatchSurvivesNetworkDisconnect { + XCTestExpectation *testExpectiation = + [self expectationWithDescription:@"testWatchSurvivesNetworkDisconnect"]; + + FIRCollectionReference *collectionRef = [self collectionRef]; + FIRDocumentReference *docRef = [collectionRef documentWithAutoID]; + + FIRFirestore *firestore = collectionRef.firestore; + + FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] + includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; + + [collectionRef addSnapshotListenerWithOptions:options + listener:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + if (!snapshot.empty && !snapshot.metadata.fromCache) { + [testExpectiation fulfill]; + } + }]; + + [firestore disableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [docRef setData:@{@"foo" : @"bar"}]; + [firestore enableNetworkWithCompletion:^(NSError *error) { + XCTAssertNil(error); + }]; + }]; + + [self awaitExpectations]; +} + +- (void)testQueriesFireFromCacheWhenOffline { + NSDictionary *testDocs = @{ + @"a" : @{@"foo" : @1}, + }; + FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs]; + + FIRQueryListenOptions *options = [[[FIRQueryListenOptions options] + includeDocumentMetadataChanges:YES] includeQueryMetadataChanges:YES]; + id<FIRListenerRegistration> registration = + [collection addSnapshotListenerWithOptions:options + listener:self.eventAccumulator.valueEventHandler]; + + FIRQuerySnapshot *querySnap = [self.eventAccumulator awaitEventWithName:@"initial event"]; + XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnap), @[ @{ @"foo" : @1 } ]); + XCTAssertEqual(querySnap.metadata.isFromCache, NO); + + [self disableNetwork]; + querySnap = [self.eventAccumulator awaitEventWithName:@"offline event with isFromCache=YES"]; + XCTAssertEqual(querySnap.metadata.isFromCache, YES); + + // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token + // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be + // sufficient. + [NSThread sleepForTimeInterval:0.2f]; + + [self enableNetwork]; + querySnap = [self.eventAccumulator awaitEventWithName:@"back online event with isFromCache=NO"]; + XCTAssertEqual(querySnap.metadata.isFromCache, NO); + + [registration remove]; +} + @end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m index 2ee3966..cc0ab29 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m @@ -18,10 +18,10 @@ #import <XCTest/XCTest.h> -#import "Firestore/Source/Core/FSTFirestoreClient.h" - #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" @interface FIRServerTimestampTests : FSTIntegrationTestCase @end @@ -42,11 +42,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 *_returnPreviousValue; + FIRSnapshotOptions *_returnEstimatedValue; } - (void)setUp { [super setUp]; + _returnPreviousValue = + [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorPrevious]; + _returnEstimatedValue = + [FIRSnapshotOptions serverTimestampBehavior:FIRServerTimestampBehaviorEstimate]; + // Data written in tests via set. _setData = @{ @"a" : @42, @@ -63,7 +72,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,8 +85,10 @@ [super tearDown]; } -// Returns the expected data, with an arbitrary timestamp substituted in. -- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp { +#pragma mark - Test Helpers + +/** Returns the expected data, with the specified timestamp substituted in. */ +- (NSDictionary *)expectedDataWithTimestamp:(nullable id)timestamp { return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} }; } @@ -88,26 +99,65 @@ XCTAssertEqualObjects(initialDataSnap.data, _initialData); } -/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */ -- (void)waitForLocalEvent { - FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."]; - XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]); +/** Waits for a snapshot with local writes. */ +- (FIRDocumentSnapshot *)waitForLocalEvent { + FIRDocumentSnapshot *snapshot; + do { + snapshot = [_accumulator awaitEventWithName:@"Local event."]; + } while (!snapshot.metadata.hasPendingWrites); + return snapshot; +} + +/** Waits for a snapshot that has no pending writes */ +- (FIRDocumentSnapshot *)waitForRemoteEvent { + FIRDocumentSnapshot *snapshot; + do { + snapshot = [_accumulator awaitEventWithName:@"Remote event."]; + } while (snapshot.metadata.hasPendingWrites); + return snapshot; +} + +/** Verifies a snapshot containing _setData but with NSNull for the timestamps. */ +- (void)verifyTimestampsAreNullInSnapshot:(FIRDocumentSnapshot *)snapshot { + XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:[NSNull null]]); +} + +/** Verifies a snapshot containing _setData but with a local estimate for the timestamps. */ +- (void)verifyTimestampsAreEstimatedInSnapshot:(FIRDocumentSnapshot *)snapshot { + id timestamp = [snapshot valueForField:@"when" options:_returnEstimatedValue]; + XCTAssertTrue([timestamp isKindOfClass:[NSDate class]]); + XCTAssertEqualObjects([snapshot dataWithOptions:_returnEstimatedValue], + [self expectedDataWithTimestamp:timestamp]); } -/** Waits for a snapshot containing _setData but with resolved server timestamps. */ -- (void)waitForRemoteEvent { - // server event should have a resolved timestamp; verify it. - FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"]; - XCTAssertTrue(remoteSnap.exists); - NSDate *when = remoteSnap[@"when"]; +/** + * Verifies a snapshot containing _setData but using the previous field value for server + * timestamps. + */ +- (void)verifyTimestampsInSnapshot:(FIRDocumentSnapshot *)snapshot + fromPreviousSnapshot:(nullable FIRDocumentSnapshot *)previousSnapshot { + if (previousSnapshot == nil) { + XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], + [self expectedDataWithTimestamp:[NSNull null]]); + } else { + XCTAssertEqualObjects([snapshot dataWithOptions:_returnPreviousValue], + [self expectedDataWithTimestamp:previousSnapshot[@"when"]]); + } +} + +/** Verifies a snapshot containing _setData but with resolved server timestamps. */ +- (void)verifySnapshotWithResolvedTimestamps:(FIRDocumentSnapshot *)snapshot { + XCTAssertTrue(snapshot.exists); + NSDate *when = snapshot[@"when"]; XCTAssertTrue([when isKindOfClass:[NSDate class]]); // Tolerate up to 10 seconds of clock skew between client and server. XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10); // Validate the rest of the document. - XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]); + XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:when]); } +/** 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 +171,102 @@ [self awaitExpectations]; } +#pragma mark - Test Cases + - (void)testServerTimestampsWorkViaSet { [self writeDocumentRef:_docRef data:_setData]; - [self waitForLocalEvent]; - [self waitForRemoteEvent]; + [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; } - (void)testServerTimestampsWorkViaUpdate { [self writeInitialData]; [self updateDocumentRef:_docRef data:_updateData]; - [self waitForLocalEvent]; - [self waitForRemoteEvent]; + [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithEstimatedValue { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsAreEstimatedInSnapshot:[self waitForLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithPreviousValue { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + + [_docRef updateData:_updateData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:remoteSnapshot]; + + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; +} + +- (void)testServerTimestampsWithPreviousValueOfDifferentType { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + XCTAssertTrue([[localSnapshot valueForField:@"a" options:_returnEstimatedValue] + isKindOfClass:[NSDate class]]); + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); + XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnPreviousValue] + isKindOfClass:[NSDate class]]); + XCTAssertTrue([[remoteSnapshot valueForField:@"a" options:_returnEstimatedValue] + isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsWithConsecutiveUpdates { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @42); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); +} + +- (void)testServerTimestampsPreviousValueFromLocalMutation { + [self writeDocumentRef:_docRef data:_setData]; + [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + + [self disableNetwork]; + + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + XCTAssertEqualObjects([localSnapshot valueForField:@"a" options:_returnPreviousValue], @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:_returnPreviousValue], @1337); + + [self enableNetwork]; + + FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[NSDate class]]); } - (void)testServerTimestampsWorkViaTransactionSet { @@ -139,7 +274,7 @@ [transaction setData:_setData forDocument:_docRef]; }]; - [self waitForRemoteEvent]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; } - (void)testServerTimestampsWorkViaTransactionUpdate { @@ -147,7 +282,7 @@ [self runTransactionBlock:^(FIRTransaction *transaction) { [transaction updateData:_updateData forDocument:_docRef]; }]; - [self waitForRemoteEvent]; + [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; } - (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m index 638835f..1874f00 100644 --- a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m @@ -40,7 +40,8 @@ - (void)testCanReadAndWriteArrayFields { [self assertSuccessfulRoundtrip:@{ - @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ] + @"array" : @[ @1, @"foo", + @{ @"deep" : @YES }, [NSNull null] ] }]; } diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m index a318c47..8b760c9 100644 --- a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m @@ -169,7 +169,7 @@ } - (void)testWritesWithIndirectlyNestedArraysSucceed { - NSDictionary<NSString *, id> *data = @{ @"nested-array" : @[ @1, @{@"foo" : @[ @2 ]} ] }; + NSDictionary<NSString *, id> *data = @{ @"nested-array" : @[ @1, @{ @"foo" : @[ @2 ] } ] }; FIRDocumentReference *ref = [self documentRef]; FIRDocumentReference *ref2 = [self documentRef]; diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m index 562c29f..5e7f6d7 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m @@ -35,6 +35,29 @@ [self awaitExpectations]; } +- (void)testCommitWithoutCompletionHandler { + FIRDocumentReference *doc = [self documentRef]; + FIRWriteBatch *batch1 = [doc.firestore batch]; + [batch1 setData:@{@"aa" : @"bb"} forDocument:doc]; + [batch1 commitWithCompletion:nil]; + FIRDocumentSnapshot *snapshot1 = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot1.exists); + XCTAssertEqualObjects(snapshot1.data, @{@"aa" : @"bb"}); + + FIRWriteBatch *batch2 = [doc.firestore batch]; + [batch2 setData:@{@"cc" : @"dd"} forDocument:doc]; + [batch2 commit]; + + // TODO(b/70631617): There's currently a backend bug that prevents us from using a resume token + // right away (against hexa at least). So we sleep. :-( :-( Anything over ~10ms seems to be + // sufficient. + [NSThread sleepForTimeInterval:0.2f]; + + FIRDocumentSnapshot *snapshot2 = [self readDocumentForRef:doc]; + XCTAssertTrue(snapshot2.exists); + XCTAssertEqualObjects(snapshot2.data, @{@"cc" : @"dd"}); +} + - (void)testSetDocuments { FIRDocumentReference *doc = [self documentRef]; XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"]; @@ -131,7 +154,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 +184,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 +218,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 +250,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/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m index 1dd6d62..53f0202 100644 --- a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m +++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; [gc addGarbageSource:referenceSet]; - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); [referenceSet addReferenceToKey:key forID:1]; FSTAssertEqualSets([gc collectGarbage], @[]); XCTAssertFalse([referenceSet isEmpty]); @@ -50,9 +50,9 @@ NS_ASSUME_NONNULL_BEGIN FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; [gc addGarbageSource:referenceSet]; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); [referenceSet addReferenceToKey:key1 forID:1]; [referenceSet addReferenceToKey:key2 forID:1]; [referenceSet addReferenceToKey:key3 forID:2]; @@ -77,12 +77,12 @@ NS_ASSUME_NONNULL_BEGIN [gc addGarbageSource:localViews]; [gc addGarbageSource:mutations]; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); [remoteTargets addReferenceToKey:key1 forID:1]; [localViews addReferenceToKey:key1 forID:1]; [mutations addReferenceToKey:key1 forID:10]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); [mutations addReferenceToKey:key2 forID:10]; XCTAssertFalse([remoteTargets isEmpty]); diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m index 90f9ca3..27c3dc3 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m @@ -70,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testEncodesMutationBatch { FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 }); FSTMutation *patch = [[FSTPatchMutation alloc] - initWithKey:[FSTDocumentKey keyWithPathString:@"bar/baz"] + initWithKey:FSTTestDocKey(@"bar/baz") fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]] value:FSTTestObjectValue( @{ @"a" : @"b", diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m index 245e1c4..45d1815 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m @@ -196,8 +196,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, NSEnumerator<NSString *> *keyPathEnumerator = keyPaths.objectEnumerator; \ [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \ FSTMaybeDocument * value, BOOL * stop) { \ - FSTDocumentKey *expectedKey = \ - [FSTDocumentKey keyWithPathString:[keyPathEnumerator nextObject]]; \ + FSTDocumentKey *expectedKey = FSTTestDocKey([keyPathEnumerator nextObject]); \ XCTAssertEqualObjects(actualKey, expectedKey); \ XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \ }]; \ @@ -213,11 +212,11 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, } while (0) /** Asserts that the given local store does not contain the given document. */ -#define FSTAssertNotContains(keyPathString) \ - do { \ - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPathString]; \ - FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ - XCTAssertNil(actual); \ +#define FSTAssertNotContains(keyPathString) \ + do { \ + FSTDocumentKey *key = FSTTestDocKey(keyPathString); \ + FSTMaybeDocument *actual = [self.localStore readDocument:key]; \ + XCTAssertNil(actual); \ } while (0) - (void)testMutationBatchKeys { @@ -261,7 +260,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, if ([self isTestBaseClass]) return; // Start a query that requires acks to be held. - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); [self allocateQuery:query]; [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})]; @@ -554,7 +553,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testCollectsGarbageAfterChangeBatch { if ([self isTestBaseClass]) return; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); [self allocateQuery:query]; FSTAssertTargetID(2); @@ -637,7 +636,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testPinsDocumentsInTheLocalView { if ([self isTestBaseClass]) return; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); [self allocateQuery:query]; FSTAssertTargetID(2); @@ -685,7 +684,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) ]]; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]); } @@ -700,7 +699,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) ]]; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects([docs values], (@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES), @@ -711,7 +710,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - (void)testCanExecuteMixedCollectionQueries { if ([self isTestBaseClass]) return; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); [self allocateQuery:query]; FSTAssertTargetID(2); @@ -736,7 +735,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, // This test only works in the absence of the FSTEagerGarbageCollector. [self restartWithNoopGarbageCollector]; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]]; + FSTQuery *query = FSTTestQuery(@"foo/bar"); FSTQueryData *queryData = [self.localStore allocateQuery:query]; FSTBoxedTargetID *targetID = @(queryData.targetID); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); @@ -770,7 +769,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, if ([self isTestBaseClass]) return; [self restartWithNoopGarbageCollector]; - FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]]; + FSTQuery *query = FSTTestQuery(@"foo"); [self allocateQuery:query]; FSTAssertTargetID(2); diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m index f168ac9..020a0a7 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m @@ -301,7 +301,7 @@ NS_ASSUME_NONNULL_BEGIN [self.persistence commitGroup:group]; NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2], batches[4] ]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo")]; + FSTQuery *query = FSTTestQuery(@"foo"); NSArray<FSTMutationBatch *> *matches = [self.mutationQueue allMutationBatchesAffectingQuery:query]; diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m index 1fed440..0b80bd9 100644 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m @@ -70,10 +70,8 @@ NS_ASSUME_NONNULL_BEGIN // Type information is currently lost in our canonicalID implementations so this currently an // easy way to force colliding canonicalIDs - FSTQuery *q1 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] - queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; - FSTQuery *q2 = [[FSTQuery queryWithPath:FSTTestPath(@"a")] - queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; + FSTQuery *q1 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))]; + FSTQuery *q2 = [FSTTestQuery(@"a") queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")]; XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID); FSTQueryData *data1 = [self queryDataWithQuery:q1 targetID:1 version:1]; @@ -141,8 +139,8 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms targetID:1 version:1]; [self addQueryData:rooms]; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/foo"); + FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/bar"); [self addMatchingKey:key1 forTargetID:rooms.targetID]; [self addMatchingKey:key2 forTargetID:rooms.targetID]; @@ -157,7 +155,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testAddOrRemoveMatchingKeys { if ([self isTestBaseClass]) return; - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); XCTAssertFalse([self.queryCache containsKey:key]); @@ -177,9 +175,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRemoveMatchingKeysForTargetID { if ([self isTestBaseClass]) return; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); [self addMatchingKey:key1 forTargetID:1]; [self addMatchingKey:key2 forTargetID:1]; @@ -207,15 +205,15 @@ NS_ASSUME_NONNULL_BEGIN FSTAssertEqualSets([garbageCollector collectGarbage], @[]); FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms") targetID:1 version:1]; - FSTDocumentKey *room1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; - FSTDocumentKey *room2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + FSTDocumentKey *room1 = FSTTestDocKey(@"rooms/bar"); + FSTDocumentKey *room2 = FSTTestDocKey(@"rooms/foo"); [self addQueryData:rooms]; [self addMatchingKey:room1 forTargetID:rooms.targetID]; [self addMatchingKey:room2 forTargetID:rooms.targetID]; FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls") targetID:2 version:1]; - FSTDocumentKey *hall1 = [FSTDocumentKey keyWithPathString:@"halls/bar"]; - FSTDocumentKey *hall2 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + FSTDocumentKey *hall1 = FSTTestDocKey(@"halls/bar"); + FSTDocumentKey *hall2 = FSTTestDocKey(@"halls/foo"); [self addQueryData:halls]; [self addMatchingKey:hall1 forTargetID:halls.targetID]; [self addMatchingKey:hall2 forTargetID:halls.targetID]; @@ -235,9 +233,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)testMatchingKeysForTargetID { if ([self isTestBaseClass]) return; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); [self addMatchingKey:key1 forTargetID:1]; [self addMatchingKey:key2 forTargetID:1]; @@ -259,8 +257,8 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms") targetID:1 purpose:FSTQueryPurposeListen]; - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"rooms/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"rooms/foo"); [self addQueryData:query1]; [self addMatchingKey:key1 forTargetID:1]; [self addMatchingKey:key2 forTargetID:1]; @@ -268,7 +266,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls") targetID:2 purpose:FSTQueryPurposeListen]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"halls/foo"]; + FSTDocumentKey *key3 = FSTTestDocKey(@"halls/foo"); [self addQueryData:query2]; [self addMatchingKey:key3 forTargetID:2]; XCTAssertEqual([self.queryCache highestTargetID], 2); diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m index 0b852a2..802117a 100644 --- a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m +++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m @@ -18,6 +18,7 @@ #import <XCTest/XCTest.h> +#import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/Model/FSTDocumentKey.h" NS_ASSUME_NONNULL_BEGIN @@ -28,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTReferenceSetTests - (void)testAddOrRemoveReferences { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"]; + FSTDocumentKey *key = FSTTestDocKey(@"foo/bar"); FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; XCTAssertTrue([referenceSet isEmpty]); @@ -53,9 +54,9 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testRemoveAllReferencesForTargetID { - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"]; - FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"]; + FSTDocumentKey *key1 = FSTTestDocKey(@"foo/bar"); + FSTDocumentKey *key2 = FSTTestDocKey(@"foo/baz"); + FSTDocumentKey *key3 = FSTTestDocKey(@"foo/blah"); FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init]; [referenceSet addReferenceToKey:key1 forID:1]; diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m index 16fe3bf..d240604 100644 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m @@ -112,7 +112,7 @@ static const int kVersion = 42; [self setTestDocumentAtPath:@"b/2"]; [self setTestDocumentAtPath:@"c/1"]; - FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"b")]; + FSTQuery *query = FSTTestQuery(@"b"); FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; NSArray *expected = @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ]; diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m index e56ab34..59f526d 100644 --- a/Firestore/Example/Tests/Model/FSTDocumentTests.m +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.m @@ -33,20 +33,20 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTDocumentTests - (void)testConstructor { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"messages/first"]; + FSTDocumentKey *key = FSTTestDocKey(@"messages/first"); FSTSnapshotVersion *version = FSTTestVersion(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); FSTDocument *doc = [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; - XCTAssertEqualObjects(doc.key, [FSTDocumentKey keyWithPathString:@"messages/first"]); + XCTAssertEqualObjects(doc.key, FSTTestDocKey(@"messages/first")); XCTAssertEqualObjects(doc.version, version); XCTAssertEqualObjects(doc.data, data); XCTAssertEqual(doc.hasLocalMutations, NO); } - (void)testExtractsFields { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"rooms/eros"]; + FSTDocumentKey *key = FSTTestDocKey(@"rooms/eros"); FSTSnapshotVersion *version = FSTTestVersion(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"desc" : @"Discuss all the project related stuff", @@ -62,38 +62,31 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testIsEqual { - FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"messages/first"]; - FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"messages/second"]; - FSTObjectValue *data1 = FSTTestObjectValue(@{ @"a" : @1 }); - FSTObjectValue *data2 = FSTTestObjectValue(@{ @"b" : @1 }); - FSTSnapshotVersion *version1 = FSTTestVersion(1); - - FSTDocument *doc1 = - [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; - FSTDocument *doc2 = - [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO]; - - XCTAssertEqualObjects(doc1, doc2); - XCTAssertEqualObjects( - doc1, [FSTDocument documentWithData:FSTTestObjectValue( - @{ @"a" : @1 }) - key:[FSTDocumentKey keyWithPathString:@"messages/first"] - version:version1 - hasLocalMutations:NO]); - - FSTSnapshotVersion *version2 = FSTTestVersion(2); - XCTAssertNotEqualObjects( - doc1, [FSTDocument documentWithData:data2 key:key1 version:version1 hasLocalMutations:NO]); - XCTAssertNotEqualObjects( - doc1, [FSTDocument documentWithData:data1 key:key2 version:version1 hasLocalMutations:NO]); - XCTAssertNotEqualObjects( - doc1, [FSTDocument documentWithData:data1 key:key1 version:version2 hasLocalMutations:NO]); - XCTAssertNotEqualObjects( - doc1, [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES]); - - XCTAssertEqualObjects( - [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES], - [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:5]); + XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"b" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/second", 1, + @{ @"b" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 2, + @{ @"a" : @1 }, NO)); + XCTAssertNotEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, NO), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, YES)); + + XCTAssertEqualObjects(FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, YES), + FSTTestDoc(@"messages/first", 1, + @{ @"a" : @1 }, 5)); } @end diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m index acf95f0..785fc6b 100644 --- a/Firestore/Example/Tests/Model/FSTFieldValueTests.m +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m @@ -26,6 +26,7 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" /** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */ @@ -39,10 +40,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 +444,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..47fa9b3 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 baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{@"bar" : @"bar-value"}; XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -54,7 +54,8 @@ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -64,13 +65,14 @@ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} }; FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO); - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"collection/key"]; + FSTDocumentKey *key = FSTTestDocKey(@"collection/key"); FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]]; FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key fieldMask:mask value:[FSTObjectValue objectValue] precondition:[FSTPrecondition none]]; - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -82,7 +84,8 @@ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil); - FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp]; + FSTMaybeDocument *patchedDoc = + [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" }; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES)); @@ -91,7 +94,8 @@ - (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 baseDocument:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(patchedDoc, baseDoc); } @@ -100,7 +104,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 baseDocument:baseDoc localWriteTime:_timestamp]; // Server timestamps aren't parsed, so we manually insert it. FSTObjectValue *expectedData = FSTTestObjectValue( @@ -108,7 +113,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 +135,10 @@ initWithVersion:FSTTestVersion(1) transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; - FSTMaybeDocument *transformedDoc = - [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; NSDictionary *expectedData = @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue}, @@ -143,7 +151,8 @@ 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 baseDocument:baseDoc localWriteTime:_timestamp]; XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0)); } @@ -154,8 +163,10 @@ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; - FSTMaybeDocument *setDoc = - [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult]; + FSTMaybeDocument *setDoc = [set applyTo:baseDoc + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); @@ -168,8 +179,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 + baseDocument:baseDoc + localWriteTime:_timestamp + mutationResult:mutationResult]; NSDictionary *expectedData = @{@"foo" : @"new-bar"}; XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO)); @@ -179,8 +192,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 \ + baseDocument:base \ + localWriteTime:_timestamp \ + mutationResult:mutationResult]; \ XCTAssertEqualObjects(actual, expected); \ } while (0); diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m index 528076f..61847b0 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m @@ -44,6 +44,7 @@ #import "Firestore/Source/Model/FSTPath.h" #import "Firestore/Source/Remote/FSTWatchChange.h" +#import "Firestore/Example/Tests/API/FSTAPIHelpers.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" NS_ASSUME_NONNULL_BEGIN @@ -266,7 +267,8 @@ NS_ASSUME_NONNULL_BEGIN @"i" : @1, @"n" : [NSNull null], @"s" : @"foo", - @"a" : @[ @2, @"bar", @{@"b" : @NO} ], + @"a" : @[ @2, @"bar", + @{ @"b" : @NO } ], @"o" : @{ @"d" : @100, @"nested" : @{@"e" : @(LLONG_MIN)}, @@ -428,7 +430,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - encodedQuery - (void)testEncodesFirstLevelKeyQueries { - FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")]; + FSTQuery *q = FSTTestQuery(@"docs/1"); FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -439,7 +441,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesFirstLevelAncestorQueries { - FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")]; + FSTQuery *q = FSTTestQuery(@"messages"); FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -455,7 +457,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesNestedAncestorQueries { - FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]; + FSTQuery *q = FSTTestQuery(@"rooms/1/messages/10/attachments"); FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -471,8 +473,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesSingleFiltersAtFirstLevelCollections { - FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] - queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; + FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))]; FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -495,7 +496,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesMultipleFiltersOnDeeperCollections { - FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + FSTQuery *q = [[FSTTestQuery(@"rooms/1/messages/10/attachments") queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))] queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")]; FSTQueryData *model = [self queryDataForQuery:q]; @@ -546,8 +547,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)unaryFilterTestWithValue:(id)value expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator) operator{ - FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] - queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; + FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)]; FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -567,7 +567,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesSortOrders { - FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] + FSTQuery *q = [FSTTestQuery(@"docs") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") ascending:YES]]; FSTQueryData *model = [self queryDataForQuery:q]; @@ -587,7 +587,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesSortOrdersDescending { - FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")] + FSTQuery *q = [FSTTestQuery(@"rooms/1/messages/10/attachments") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop") ascending:NO]]; FSTQueryData *model = [self queryDataForQuery:q]; @@ -607,7 +607,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesLimits { - FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26]; + FSTQuery *q = [FSTTestQuery(@"docs") queryBySettingLimit:26]; FSTQueryData *model = [self queryDataForQuery:q]; GCFSTarget *expected = [GCFSTarget message]; @@ -624,7 +624,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesResumeTokens { - FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")]; + FSTQuery *q = FSTTestQuery(@"docs"); FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q targetID:1 purpose:FSTQueryPurposeListen diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m index 2c1b8db..3abcb48 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m @@ -103,11 +103,11 @@ static NSString *const kNoIOSTag = @"no-ios"; - (nullable FSTQuery *)parseQuery:(id)querySpec { if ([querySpec isKindOfClass:[NSString class]]) { - return [FSTQuery queryWithPath:[FSTResourcePath pathWithString:querySpec]]; + return FSTTestQuery(querySpec); } else if ([querySpec isKindOfClass:[NSDictionary class]]) { NSDictionary *queryDict = (NSDictionary *)querySpec; NSString *path = queryDict[@"path"]; - __block FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithString:path]]; + __block FSTQuery *query = FSTTestQuery(path); if (queryDict[@"limit"]) { NSNumber *limit = queryDict[@"limit"]; query = [query queryBySettingLimit:limit.integerValue]; @@ -156,7 +156,7 @@ static NSString *const kNoIOSTag = @"no-ios"; FSTTargetID actualID = [self.driver addUserListenerWithQuery:query]; FSTTargetID expectedID = [listenSpec[0] intValue]; - XCTAssertEqual(actualID, expectedID); + XCTAssertEqual(actualID, expectedID, @"targetID assigned to listen"); } - (void)doUnlisten:(NSArray *)unlistenSpec { @@ -237,7 +237,7 @@ static NSString *const kNoIOSTag = @"no-ios"; } } else if (watchEntity[@"doc"]) { NSArray *docSpec = watchEntity[@"doc"]; - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:docSpec[0]]; + FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; FSTMaybeDocument *doc = @@ -249,7 +249,7 @@ static NSString *const kNoIOSTag = @"no-ios"; document:doc]; [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]]; } else if (watchEntity[@"key"]) { - FSTDocumentKey *docKey = [FSTDocumentKey keyWithPathString:watchEntity[@"key"]]; + FSTDocumentKey *docKey = FSTTestDocKey(watchEntity[@"key"]); FSTWatchChange *change = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] removedTargetIDs:watchEntity[@"removedTargets"] diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index 3d031bd..7cb2726 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -14,6 +14,7 @@ * limitations under the License. */ +#import <Firestore/Source/Remote/FSTRemoteStore.h> #import <Foundation/Foundation.h> #import "Firestore/Source/Core/FSTTypes.h" @@ -76,7 +77,7 @@ typedef NSDictionary<FSTUser *, NSArray<FSTOutstandingWrite *> *> FSTOutstanding * * Each method on the driver injects a different event into the system. */ -@interface FSTSyncEngineTestDriver : NSObject +@interface FSTSyncEngineTestDriver : NSObject <FSTOnlineStateDelegate> /** * Initializes the underlying FSTSyncEngine with the given local persistence implementation and diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m index 896a292..da63933 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m @@ -119,7 +119,7 @@ NS_ASSUME_NONNULL_BEGIN _remoteStore.syncEngine = _syncEngine; _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; - _remoteStore.onlineStateDelegate = _eventManager; + _remoteStore.onlineStateDelegate = self; // Set up internal event tracking for the spec tests. NSMutableArray<FSTQueryEvent *> *events = [NSMutableArray array]; @@ -139,6 +139,11 @@ NS_ASSUME_NONNULL_BEGIN return self; } +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; +} + - (void)start { [self.localStore start]; [self.remoteStore start]; diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json index e607710..7bfe557 100644 --- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -1608,7 +1608,19 @@ "stateExpect": { "activeTargets": {}, "limboDocs": [] - } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] }, { "enableNetwork": true, diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json index f542a6e..3981cec 100644 --- a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -294,5 +294,170 @@ ] } ] + }, + "Queries revert to fromCache=true when offline.": { + "describeName": "Offline:", + "itName": "Queries revert to fromCache=true when offline.", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + } + }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] } } diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h index ae5392c..baa501b 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.h +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN -typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error); +typedef void (^FSTValueEventHandler)(id _Nullable, NSError *_Nullable error); @interface FSTEventAccumulator : NSObject @@ -35,7 +35,8 @@ 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; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m index b44ec67..c4c1602 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.m +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m @@ -68,9 +68,8 @@ NS_ASSUME_NONNULL_BEGIN return events[0]; } -// Overrides the handler property -- (void (^)(id _Nullable, NSError *))handler { - return ^void(id _Nullable value, NSError *error) { +- (void (^)(id _Nullable, NSError *_Nullable))valueEventHandler { + return ^void(id _Nullable value, NSError *_Nullable error) { // We can't store nil in the _events array, but these are still interesting to tests so store // NSNull instead. id event = value ? value : [NSNull null]; diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index 91ccbcf..4dbf910 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -16,7 +16,6 @@ #import <Foundation/Foundation.h> -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" #import "Firestore/Source/Model/FSTDocumentKeySet.h" @@ -33,7 +32,6 @@ @class FSTPatchMutation; @class FSTQuery; @class FSTRemoteEvent; -@class FSTResourceName; @class FSTResourcePath; @class FSTSetMutation; @class FSTSnapshotVersion; @@ -145,6 +143,8 @@ NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int seco */ NSData *FSTTestData(int bytes, ...); +// Note that FIRGeoPoint is a model class in addition to an API class, so we put this helper here +// instead of FSTAPIHelpers.h /** Creates a new GeoPoint from the latitude and longitude values */ FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m index f01bddb..f2b3605 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.m +++ b/Firestore/Example/Tests/Util/FSTHelpers.m @@ -24,6 +24,7 @@ #import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTTimestamp.h" #import "Firestore/Source/Core/FSTView.h" +#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDatabaseID.h" @@ -138,7 +139,7 @@ FSTDocument *FSTTestDoc(NSString *path, FSTTestSnapshotVersion version, NSDictionary<NSString *, id> *data, BOOL hasMutations) { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + FSTDocumentKey *key = FSTTestDocKey(path); return [FSTDocument documentWithData:FSTTestObjectValue(data) key:key version:FSTTestVersion(version) @@ -146,7 +147,7 @@ FSTDocument *FSTTestDoc(NSString *path, } FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path]; + FSTDocumentKey *key = FSTTestDocKey(path); return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; } @@ -214,7 +215,7 @@ FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) { } NSComparator FSTTestDocComparator(NSString *fieldPath) { - FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]] + FSTQuery *query = [FSTTestQuery(@"docs") queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath) ascending:YES]]; return [query comparator]; @@ -229,7 +230,7 @@ FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs) { } FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values) { - return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + return [[FSTSetMutation alloc] initWithKey:FSTTestDocKey(path) value:FSTTestObjectValue(values) precondition:[FSTPrecondition none]]; } @@ -274,7 +275,7 @@ FSTTransformMutation *FSTTestTransformMutation(NSString *path, } FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { - return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path] + return [[FSTDeleteMutation alloc] initWithKey:FSTTestDocKey(path) precondition:[FSTPrecondition none]]; } @@ -334,12 +335,12 @@ FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, NSArray<NSString *> *removedKeys) { FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; for (NSString *keyPath in addedKeys) { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + FSTDocumentKey *key = FSTTestDocKey(keyPath); added = [added setByAddingObject:key]; } FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; for (NSString *keyPath in removedKeys) { - FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath]; + FSTDocumentKey *key = FSTTestDocKey(keyPath); removed = [removed setByAddingObject:key]; } return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index 88f9346..ac54253 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -14,6 +14,7 @@ * limitations under the License. */ +#import <Firestore/Source/Core/FSTTypes.h> #import <Foundation/Foundation.h> #import <XCTest/XCTest.h> @@ -82,6 +83,10 @@ extern "C" { - (void)deleteDocumentRef:(FIRDocumentReference *)ref; +- (void)disableNetwork; + +- (void)enableNetwork; + /** * "Blocks" the current thread/run loop until the block returns YES. * Should only be called on the main thread. diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 3d30a77..839e4a5 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -18,6 +18,7 @@ #import <FirebaseCore/FIRLogger.h> #import <FirebaseFirestore/FirebaseFirestore-umbrella.h> +#import <Firestore/Source/Core/FSTFirestoreClient.h> #import <GRPCClient/GRPCCall+ChannelArg.h> #import <GRPCClient/GRPCCall+Tests.h> @@ -158,11 +159,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)shutdownFirestore:(FIRFirestore *)firestore { - XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"]; - [firestore shutdownWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [shutdownCompletion fulfill]; - }]; + [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"shutdown"]]; [self awaitExpectations]; } @@ -261,31 +258,29 @@ NS_ASSUME_NONNULL_BEGIN } - (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data { - XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; - [ref setData:data - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; + [ref setData:data completion:[self completionForExpectationWithName:@"setData"]]; [self awaitExpectations]; } - (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)data { - XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"]; - [ref updateData:data - completion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; + [ref updateData:data completion:[self completionForExpectationWithName:@"updateData"]]; [self awaitExpectations]; } - (void)deleteDocumentRef:(FIRDocumentReference *)ref { - XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"]; - [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; + [ref deleteDocumentWithCompletion:[self completionForExpectationWithName:@"deleteDocument"]]; + [self awaitExpectations]; +} + +- (void)disableNetwork { + [self.db.client + disableNetworkWithCompletion:[self completionForExpectationWithName:@"Disable Network."]]; + [self awaitExpectations]; +} + +- (void)enableNetwork { + [self.db.client + enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable Network."]]; [self awaitExpectations]; } diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h index 9d575f9..7a8feb8 100644 --- a/Firestore/Example/Tests/Util/XCTestCase+Await.h +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.h @@ -14,6 +14,7 @@ * limitations under the License. */ +#import <Firestore/Source/Core/FSTTypes.h> #import <XCTest/XCTest.h> @interface XCTestCase (Await) @@ -29,4 +30,10 @@ */ - (double)defaultExpectationWaitSeconds; +/** + * Returns a completion block that fulfills a newly-created expectation with the specified + * name. + */ +- (FSTVoidErrorBlock)completionForExpectationWithName:(NSString *)expectationName; + @end diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m index 15c67ca..7f4356c 100644 --- a/Firestore/Example/Tests/Util/XCTestCase+Await.m +++ b/Firestore/Example/Tests/Util/XCTestCase+Await.m @@ -35,4 +35,12 @@ static const double kExpectationWaitSeconds = 10.0; return kExpectationWaitSeconds; } +- (FSTVoidErrorBlock)completionForExpectationWithName:(NSString *)expectationName { + XCTestExpectation *expectation = [self expectationWithDescription:expectationName]; + return ^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }; +} + @end diff --git a/Firestore/Source/API/FIRCollectionReference.mm b/Firestore/Source/API/FIRCollectionReference.mm index 92cccc6..70a14c2 100644 --- a/Firestore/Source/API/FIRCollectionReference.mm +++ b/Firestore/Source/API/FIRCollectionReference.mm @@ -15,6 +15,7 @@ */ #import "FIRCollectionReference.h" +#import "FIRFirestore.h" #include "Firestore/core/src/firebase/firestore/util/autoid.h" @@ -65,6 +66,29 @@ NS_ASSUME_NONNULL_BEGIN FSTFail(@"Use FIRCollectionReference initWithPath: initializer."); } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToReference:other]; +} + +- (BOOL)isEqualToReference:(nullable FIRCollectionReference *)reference { + if (self == reference) return YES; + if (reference == nil) return NO; + if (self.firestore != reference.firestore && ![self.firestore isEqual:reference.firestore]) + return NO; + if (self.query != reference.query && ![self.query isEqual:reference.query]) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.query hash]; + return hash; +} + - (NSString *)collectionID { return [self.query.path lastSegment]; } diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m index 970dc90..d1d9999 100644 --- a/Firestore/Source/API/FIRDocumentChange.m +++ b/Firestore/Source/API/FIRDocumentChange.m @@ -57,11 +57,11 @@ NS_ASSUME_NONNULL_BEGIN NSUInteger index = 0; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRDocumentSnapshot *document = - [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, @"Invalid event type for first snapshot"); FSTAssert(!lastDocument || @@ -79,11 +79,11 @@ NS_ASSUME_NONNULL_BEGIN FSTDocumentSet *indexTracker = snapshot.oldDocuments; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRDocumentSnapshot *document = - [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; NSUInteger oldIndex = NSNotFound; NSUInteger newIndex = NSNotFound; @@ -112,7 +112,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation FIRDocumentChange - (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRDocumentSnapshot *)document + document:(FIRQueryDocumentSnapshot *)document oldIndex:(NSUInteger)oldIndex newIndex:(NSUInteger)newIndex { if (self = [super init]) { diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m index 1c80ea9..87e6631 100644 --- a/Firestore/Source/API/FIRDocumentReference.m +++ b/Firestore/Source/API/FIRDocumentReference.m @@ -48,6 +48,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges NS_DESIGNATED_INITIALIZER; +@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; + @end @implementation FIRDocumentListenOptions @@ -114,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToReference:other]; } diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m index b78472e..358ddac 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -20,11 +20,13 @@ #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/FSTAssert.h" #import "Firestore/Source/Util/FSTUsageValidation.h" NS_ASSUME_NONNULL_BEGIN @@ -76,6 +78,37 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. + if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + if (self.firestore != snapshot.firestore && ![self.firestore isEqual:snapshot.firestore]) + return NO; + if (self.internalKey != snapshot.internalKey && ![self.internalKey isEqual:snapshot.internalKey]) + return NO; + if (self.internalDocument != snapshot.internalDocument && + ![self.internalDocument isEqual:snapshot.internalDocument]) + return NO; + if (self.fromCache != snapshot.fromCache) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.internalKey hash]; + hash = hash * 31u + [self.internalDocument hash]; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + @dynamic exists; - (BOOL)exists { @@ -99,40 +132,48 @@ NS_ASSUME_NONNULL_BEGIN return _cachedMetadata; } -- (NSDictionary<NSString *, id> *)data { - FSTDocument *document = self.internalDocument; - - if (!document) { - FSTThrowInvalidUsage( - @"NonExistentDocumentException", - @"Document '%@' doesn't exist. " - @"Check document.exists to make sure the document exists before calling document.data.", - self.internalKey); - } +- (nullable NSDictionary<NSString *, id> *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} - return [self convertedObject:[self.internalDocument data]]; +- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { + return self.internalDocument == nil + ? nil + : [self convertedObject:[self.internalDocument data] + options:[FSTFieldValueOptions optionsForSnapshotOptions: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 fieldValue == nil + ? nil + : [self convertedValue:fieldValue + options:[FSTFieldValueOptions optionsForSnapshotOptions: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 +187,69 @@ 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; } @end +@interface FIRQueryDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryDocumentSnapshot + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache { + self = [super initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; + return self; +} + +- (NSDictionary<NSString *, id> *)data { + NSDictionary<NSString *, id> *data = [super data]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +- (NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { + NSDictionary<NSString *, id> *data = [super dataWithOptions:options]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m index d0a70c0..f4e532f 100644 --- a/Firestore/Source/API/FIRFieldPath.m +++ b/Firestore/Source/API/FIRFieldPath.m @@ -80,7 +80,7 @@ NS_ASSUME_NONNULL_BEGIN return [[[self class] alloc] initPrivate:self.internalValue]; } -- (BOOL)isEqual:(id)object { +- (BOOL)isEqual:(nullable id)object { if (self == object) { return YES; } diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m index 7814ce1..9df3711 100644 --- a/Firestore/Source/API/FIRFirestore.m +++ b/Firestore/Source/API/FIRFirestore.m @@ -50,13 +50,17 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; @property(nonatomic, strong) id<FSTCredentialsProvider> credentialsProvider; @property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; -@property(nonatomic, strong) FSTFirestoreClient *client; +// Note that `client` is updated after initialization, but marking this readwrite would generate an +// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; @property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; @end @implementation FIRFirestore { + // All guarded by @synchronized(self) FIRFirestoreSettings *_settings; + FSTFirestoreClient *_client; } + (NSMutableDictionary<NSString *, FIRFirestore *> *)instances { @@ -154,64 +158,74 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; } - (FIRFirestoreSettings *)settings { - // Disallow mutation of our internal settings - return [_settings copy]; + @synchronized(self) { + // Disallow mutation of our internal settings + return [_settings copy]; + } } - (void)setSettings:(FIRFirestoreSettings *)settings { - // As a special exception, don't throw if the same settings are passed repeatedly. This should - // make it more friendly to create a Firestore instance. - if (_client && ![_settings isEqual:settings]) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"Firestore instance has already been started and its settings can no " - "longer be changed. You can only set settings before calling any " - "other methods on a Firestore instance."); + @synchronized(self) { + // As a special exception, don't throw if the same settings are passed repeatedly. This should + // make it more friendly to create a Firestore instance. + if (_client && ![_settings isEqual:settings]) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"Firestore instance has already been started and its settings can no " + "longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + _settings = [settings copy]; } - _settings = [settings copy]; } /** - * Ensures that the FirestoreClient is configured. - * @return self + * Ensures that the FirestoreClient is configured and returns it. */ -- (instancetype)firestoreWithConfiguredClient { - if (!_client) { - // These values are validated elsewhere; this is just double-checking: - FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); - FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); - - FSTDatabaseInfo *databaseInfo = - [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID - persistenceKey:_persistenceKey - host:_settings.host - sslEnabled:_settings.sslEnabled]; - - FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; - - _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo - usePersistence:_settings.persistenceEnabled - credentialsProvider:_credentialsProvider - userDispatchQueue:userDispatchQueue - workerDispatchQueue:_workerDispatchQueue]; +- (FSTFirestoreClient *)client { + [self ensureClientConfigured]; + return _client; +} + +- (void)ensureClientConfigured { + @synchronized(self) { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); + + FSTDatabaseInfo *databaseInfo = + [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID + persistenceKey:_persistenceKey + host:_settings.host + sslEnabled:_settings.sslEnabled]; + + FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + + _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo + usePersistence:_settings.persistenceEnabled + credentialsProvider:_credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:_workerDispatchQueue]; + } } - return self; } - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { if (!collectionPath) { FSTThrowInvalidArgument(@"Collection path cannot be nil."); } + [self ensureClientConfigured]; FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; - return - [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; + return [FIRCollectionReference referenceWithPath:path firestore:self]; } - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { if (!documentPath) { FSTThrowInvalidArgument(@"Document path cannot be nil."); } + [self ensureClientConfigured]; FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; - return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; + return [FIRDocumentReference referenceWithPath:path firestore:self]; } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock @@ -241,12 +255,13 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; internalCompletion(result, error); }); }; - [self firestoreWithConfiguredClient]; [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; } - (FIRWriteBatch *)batch { - return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]]; + [self ensureClientConfigured]; + + return [FIRWriteBatch writeBatchWithFirestore:self]; } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock @@ -264,11 +279,19 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; } - (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - if (!self.client) { + FSTFirestoreClient *client; + @synchronized(self) { + client = _client; + _client = nil; + } + + if (!client) { + // We should be dispatching the callback on the user dispatch queue but if the client is nil + // here that queue was never created. completion(nil); - return; + } else { + [client shutdownWithCompletion:completion]; } - return [self.client shutdownWithCompletion:completion]; } + (BOOL)isLoggingEnabled { @@ -279,6 +302,16 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); } +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self ensureClientConfigured]; + [self.client enableNetworkWithCompletion:completion]; +} + +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { + [self ensureClientConfigured]; + [self.client disableNetworkWithCompletion:completion]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m index 12e79c5..2feca39 100644 --- a/Firestore/Source/API/FIRQuery.m +++ b/Firestore/Source/API/FIRQuery.m @@ -107,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToQuery:other]; } @@ -256,6 +256,95 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions value:value]; } +- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { + NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; + if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { + FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); + } + NSString *path; + id value = nil; + if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && + [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { + path = comparison.leftExpression.keyPath; + value = comparison.rightExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && + [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { + path = comparison.rightExpression.keyPath; + value = comparison.leftExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else { + FSTThrowInvalidArgument( + @"Invalid query. Predicate comparisons must include a key path and a constant."); + } + // Fallback cases of unsupported comparison operator. + switch (comparison.predicateOperatorType) { + case NSCustomSelectorPredicateOperatorType: + FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); + break; + default: + FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", + (unsigned long)comparison.predicateOperatorType); + } +} + +- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { + FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); + } + FIRQuery *query = self; + for (NSPredicate *pred in compound.subpredicates) { + query = [query queryFilteredUsingPredicate:pred]; + } + return query; +} + +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + return [self queryFilteredUsingComparisonPredicate:predicate]; + } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + return [self queryFilteredUsingCompoundPredicate:predicate]; + } else if ([predicate isKindOfClass:[[NSPredicate + predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { + return true; + }] class]]) { + FSTThrowInvalidArgument( + @"Invalid query. Block-based predicates are not " + "supported. Please use predicateWithFormat to " + "create predicates instead."); + } else { + FSTThrowInvalidArgument( + @"Invalid query. Expect comparison or compound of " + "comparison predicate. Please use " + "predicateWithFormat to create predicates."); + } +} + - (FIRQuery *)queryOrderedByField:(NSString *)field { return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m index 6bc6761..177e461 100644 --- a/Firestore/Source/API/FIRQuerySnapshot.m +++ b/Firestore/Source/API/FIRQuerySnapshot.m @@ -16,6 +16,7 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "FIRFirestore.h" #import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRDocumentChange+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" @@ -57,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation FIRQuerySnapshot { // Cached value of the documents property. - NSArray<FIRDocumentSnapshot *> *_documents; + NSArray<FIRQueryDocumentSnapshot *> *_documents; // Cached value of the documentChanges property. NSArray<FIRDocumentChange *> *_documentChanges; @@ -76,6 +77,35 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + if (self.firestore != snapshot.firestore && ![self.firestore isEqual:snapshot.firestore]) + return NO; + if (self.originalQuery != snapshot.originalQuery && + ![self.originalQuery isEqual:snapshot.originalQuery]) + return NO; + if (self.snapshot != snapshot.snapshot && ![self.snapshot isEqual:snapshot.snapshot]) return NO; + if (self.metadata != snapshot.metadata && ![self.metadata isEqual:snapshot.metadata]) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.originalQuery hash]; + hash = hash * 31u + [self.snapshot hash]; + hash = hash * 31u + [self.metadata hash]; + return hash; +} + @dynamic empty; - (FIRQuery *)query { @@ -93,18 +123,18 @@ NS_ASSUME_NONNULL_BEGIN return self.snapshot.documents.count; } -- (NSArray<FIRDocumentSnapshot *> *)documents { +- (NSArray<FIRQueryDocumentSnapshot *> *)documents { if (!_documents) { FSTDocumentSet *documentSet = self.snapshot.documents; FIRFirestore *firestore = self.firestore; BOOL fromCache = self.metadata.fromCache; - NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array]; + NSMutableArray<FIRQueryDocumentSnapshot *> *result = [NSMutableArray array]; for (FSTDocument *document in documentSet.documentEnumerator) { - [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:document.key - document:document - fromCache:fromCache]]; + [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; } _documents = result; 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/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m index 224015f..d957a8d 100644 --- a/Firestore/Source/API/FIRSnapshotMetadata.m +++ b/Firestore/Source/API/FIRSnapshotMetadata.m @@ -44,6 +44,28 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToMetadata:other]; +} + +- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { + if (self == metadata) return YES; + if (metadata == nil) return NO; + if (self.pendingWrites != metadata.pendingWrites) return NO; + if (self.fromCache != metadata.fromCache) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = self.pendingWrites ? 1 : 0; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions+Internal.h b/Firestore/Source/API/FIRSnapshotOptions+Internal.h new file mode 100644 index 0000000..64e7dbc --- /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> + +#import "Firestore/Source/Model/FSTFieldValue.h" + +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:(FSTServerTimestampBehavior)serverTimestampBehavior; + +/* Returns the server timestamp behavior. Returns -1 if no behavior is specified. */ +- (FSTServerTimestampBehavior)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..72ea3cc --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions.m @@ -0,0 +1,72 @@ +/* + * 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 "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions () + +@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; + +@end + +@implementation FIRSnapshotOptions + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)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:FSTServerTimestampBehaviorNone]; + }); + + return sharedInstance; +} + ++ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { + switch (serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; + case FIRServerTimestampBehaviorNone: + return [FIRSnapshotOptions defaultOptions]; + 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/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m index b918a9a..b1cfa09 100644 --- a/Firestore/Source/API/FIRWriteBatch.m +++ b/Firestore/Source/API/FIRWriteBatch.m @@ -93,7 +93,11 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion { +- (void)commit { + [self commitWithCompletion:nil]; +} + +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { [self verifyNotCommitted]; self.committed = TRUE; [self.firestore.client writeMutations:self.mutations completion:completion]; diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h index edd2a96..8eafd4b 100644 --- a/Firestore/Source/Core/FSTEventManager.h +++ b/Firestore/Source/Core/FSTEventManager.h @@ -62,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot; - (void)queryDidError:(NSError *)error; -- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState; +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; @property(nonatomic, strong, readonly) FSTQuery *query; diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m index 3e1b99b..bc204a0 100644 --- a/Firestore/Source/Core/FSTEventManager.m +++ b/Firestore/Source/Core/FSTEventManager.m @@ -151,7 +151,7 @@ NS_ASSUME_NONNULL_BEGIN self.viewSnapshotHandler(nil, error); } -- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState { +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { self.onlineState = onlineState; if (self.snapshot && !self.raisedInitialEvent && [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { @@ -268,7 +268,7 @@ NS_ASSUME_NONNULL_BEGIN } [queryInfo.listeners addObject:listener]; - [listener clientDidChangeOnlineState:self.onlineState]; + [listener applyChangedOnlineState:self.onlineState]; if (queryInfo.viewSnapshot) { [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; @@ -321,11 +321,11 @@ NS_ASSUME_NONNULL_BEGIN [self.queries removeObjectForKey:query]; } -- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState { +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { self.onlineState = onlineState; for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { for (FSTQueryListener *listener in info.listeners) { - [listener clientDidChangeOnlineState:onlineState]; + [listener applyChangedOnlineState:onlineState]; } } } diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index 6a1e11b..0ecf2f6 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN * SDK architecture. It is responsible for creating the worker queue that is shared by all of the * other components in the system. */ -@interface FSTFirestoreClient : NSObject +@interface FSTFirestoreClient : NSObject <FSTOnlineStateDelegate> /** * Creates and returns a FSTFirestoreClient with the given parameters. diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m index 2e0e407..fff644d 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.m +++ b/Firestore/Source/Core/FSTFirestoreClient.m @@ -172,7 +172,7 @@ NS_ASSUME_NONNULL_BEGIN // Setup wiring for remote store. _remoteStore.syncEngine = _syncEngine; - _remoteStore.onlineStateDelegate = _eventManager; + _remoteStore.onlineStateDelegate = self; // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation // queue, etc.) so must be started after LocalStore. @@ -187,6 +187,11 @@ NS_ASSUME_NONNULL_BEGIN [self.syncEngine userDidChange:user]; } +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; +} + - (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { [self.workerDispatchQueue dispatchAsync:^{ [self.remoteStore disableNetwork]; diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m index 0bfd917..13657f7 100644 --- a/Firestore/Source/Core/FSTQuery.m +++ b/Firestore/Source/Core/FSTQuery.m @@ -205,7 +205,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self.field isEqual:((FSTNullFilter *)other).field]; } @@ -246,7 +246,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self.field isEqual:((FSTNanFilter *)other).field]; } diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h index bb45196..7060155 100644 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -100,6 +100,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)userDidChange:(FSTUser *)user; +/** Applies an FSTOnlineState change to the sync engine and notifies any views of the change. */ +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.m b/Firestore/Source/Core/FSTSyncEngine.m index 98658e4..27ab73e 100644 --- a/Firestore/Source/Core/FSTSyncEngine.m +++ b/Firestore/Source/Core/FSTSyncEngine.m @@ -318,6 +318,21 @@ NS_ASSUME_NONNULL_BEGIN [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent]; } +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + NSMutableArray<FSTViewSnapshot *> *newViewSnapshots = [NSMutableArray array]; + [self.queryViewsByQuery + enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) { + FSTViewChange *viewChange = [queryView.view applyChangedOnlineState:onlineState]; + FSTAssert(viewChange.limboChanges.count == 0, + @"OnlineState should not affect limbo documents."); + if (viewChange.snapshot) { + [newViewSnapshots addObject:viewChange.snapshot]; + } + }]; + + [self.delegate handleViewSnapshots:newViewSnapshots]; +} + - (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { [self assertDelegateExistsForSelector:_cmd]; diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h index c10f1bf..b47bd0b 100644 --- a/Firestore/Source/Core/FSTTypes.h +++ b/Firestore/Source/Core/FSTTypes.h @@ -67,8 +67,8 @@ typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, typedef NS_ENUM(NSUInteger, FSTOnlineState) { /** * The Firestore client is in an unknown online state. This means the client is either not - * actively trying to establish a connection or it was previously in an unknown state and is - * trying to establish a connection. + * actively trying to establish a connection or it is currently trying to establish a connection, + * but it has not succeeded or failed yet. */ FSTOnlineStateUnknown, @@ -80,9 +80,8 @@ typedef NS_ENUM(NSUInteger, FSTOnlineState) { FSTOnlineStateHealthy, /** - * The client has tried to establish a connection but has failed. - * This state is reached after either a connection attempt failed or a healthy stream was closed - * for unexpected reasons. + * The client considers itself offline. It is either trying to establish a connection but + * failing, or it has been explicitly marked offline via a call to `disableNetwork`. */ FSTOnlineStateFailed }; diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index ed230a3..6ff77cd 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -16,6 +16,7 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" #import "Firestore/Source/Model/FSTDocumentKeySet.h" @@ -138,6 +139,12 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges targetChange:(nullable FSTTargetChange *)targetChange; +/** + * Applies an FSTOnlineState change to the view, potentially generating an FSTViewChange if the + * view's syncState changes as a result. + */ +- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m index 9b44bf4..78019c6 100644 --- a/Firestore/Source/Core/FSTView.m +++ b/Firestore/Source/Core/FSTView.m @@ -94,6 +94,12 @@ NS_ASSUME_NONNULL_BEGIN return self.type == otherChange.type && [self.key isEqual:otherChange.key]; } +- (NSUInteger)hash { + NSUInteger hash = self.type; + hash = hash * 31u + [self.key hash]; + return hash; +} + @end #pragma mark - FSTViewChange @@ -330,6 +336,24 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang } } +- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState { + if (self.isCurrent && onlineState == FSTOnlineStateFailed) { + // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState + // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange + // that sets `current` back to YES once the client is back online. + self.current = NO; + return + [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] + initWithDocumentSet:self.documentSet + changeSet:[FSTDocumentViewChangeSet changeSet] + needsRefill:NO + mutatedKeys:self.mutatedKeys]]; + } else { + // No effect, just return a no-op FSTViewChange. + return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; + } +} + #pragma mark - Private methods /** Returns whether the doc for the given key should be in limbo. */ diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m index 1631789..25a5935 100644 --- a/Firestore/Source/Local/FSTDocumentReference.m +++ b/Firestore/Source/Local/FSTDocumentReference.m @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; FSTDocumentReference *reference = (FSTDocumentReference *)other; diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index fb1c81a..83b932c 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -72,8 +72,8 @@ using leveldb::WriteOptions; #else #error "local storage on tvOS" -// TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches -// https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/ + // TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches + // https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/ #endif } diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m index 4d0448a..bff5855 100644 --- a/Firestore/Source/Model/FSTDatabaseID.m +++ b/Firestore/Source/Model/FSTDatabaseID.m @@ -48,7 +48,7 @@ NSString *const kDefaultDatabaseID = @"(default)"; - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToDatabaseId:other]; } diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h index 6de9793..93fd5c4 100644 --- a/Firestore/Source/Model/FSTFieldValue.h +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -22,7 +22,9 @@ @class FSTDocumentKey; @class FSTFieldPath; @class FSTTimestamp; +@class FSTFieldValueOptions; @class FIRGeoPoint; +@class FIRSnapshotOptions; NS_ASSUME_NONNULL_BEGIN @@ -40,6 +42,32 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { FSTTypeOrderObject, }; +/** Defines the return value for pending server timestamps. */ +typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { + FSTServerTimestampBehaviorNone, + FSTServerTimestampBehaviorEstimate, + FSTServerTimestampBehaviorPrevious +}; + +/** Holds properties that define field value deserialization options. */ +@interface FSTFieldValueOptions : NSObject + +@property(nonatomic, readonly, assign) FSTServerTimestampBehavior serverTimestampBehavior; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates an FSTFieldValueOptions instance that specifies deserialization behavior for pending + * server timestamps. + */ +- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior + NS_DESIGNATED_INITIALIZER; + +/** Creates an FSTFieldValueOption instance from FIRSnapshotOptions. */ ++ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)value; + +@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. @@ -58,7 +86,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * - Array * - Object */ -@interface FSTFieldValue : NSObject +@interface FSTFieldValue <__covariant T> : NSObject /** Returns the FSTTypeOrder for this value. */ - (FSTTypeOrder)typeOrder; @@ -69,7 +97,15 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * TODO(mikelehen): This conversion should probably happen at the API level and right now `value` is * used inappropriately in the serializer implementation, etc. We need to do some reworking. */ -- (id)value; +- (T)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). + */ +- (T)valueWithOptions:(FSTFieldValueOptions *)options; /** Compares against another FSTFieldValue. */ - (NSComparisonResult)compare:(FSTFieldValue *)other; @@ -79,26 +115,24 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { /** * A null value stored in Firestore. The |value| of a FSTNullValue is [NSNull null]. */ -@interface FSTNullValue : FSTFieldValue +@interface FSTNullValue : FSTFieldValue <NSNull *> + (instancetype)nullValue; -- (id)value; @end /** * A boolean value stored in Firestore. */ -@interface FSTBooleanValue : FSTFieldValue +@interface FSTBooleanValue : FSTFieldValue <NSNumber *> + (instancetype)trueValue; + (instancetype)falseValue; + (instancetype)booleanValue:(BOOL)value; -- (NSNumber *)value; @end /** * Base class inherited from by FSTIntegerValue and FSTDoubleValue. It implements proper number * comparisons between the two types. */ -@interface FSTNumberValue : FSTFieldValue +@interface FSTNumberValue : FSTFieldValue <NSNumber *> @end /** @@ -106,7 +140,6 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTIntegerValue : FSTNumberValue + (instancetype)integerValue:(int64_t)value; -- (NSNumber *)value; - (int64_t)internalValue; @end @@ -116,24 +149,21 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { @interface FSTDoubleValue : FSTNumberValue + (instancetype)doubleValue:(double)value; + (instancetype)nanValue; -- (NSNumber *)value; - (double)internalValue; @end /** * A string stored in Firestore. */ -@interface FSTStringValue : FSTFieldValue +@interface FSTStringValue : FSTFieldValue <NSString *> + (instancetype)stringValue:(NSString *)value; -- (NSString *)value; @end /** * A timestamp value stored in Firestore. */ -@interface FSTTimestampValue : FSTFieldValue +@interface FSTTimestampValue : FSTFieldValue <NSDate *> + (instancetype)timestampValue:(FSTTimestamp *)value; -- (NSDate *)value; - (FSTTimestamp *)internalValue; @end @@ -144,46 +174,54 @@ 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; +@interface FSTServerTimestampValue : FSTFieldValue <id> ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue; + @property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@property(nonatomic, strong, readonly, nullable) FSTFieldValue *previousValue; + @end /** * A geo point value stored in Firestore. */ -@interface FSTGeoPointValue : FSTFieldValue +@interface FSTGeoPointValue : FSTFieldValue <FIRGeoPoint *> + (instancetype)geoPointValue:(FIRGeoPoint *)value; -- (FIRGeoPoint *)value; +- (FIRGeoPoint *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** * A blob value stored in Firestore. */ -@interface FSTBlobValue : FSTFieldValue +@interface FSTBlobValue : FSTFieldValue <NSData *> + (instancetype)blobValue:(NSData *)value; -- (NSData *)value; +- (NSData *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** * A reference value stored in Firestore. */ -@interface FSTReferenceValue : FSTFieldValue +@interface FSTReferenceValue : FSTFieldValue <FSTDocumentKey *> + (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID; -- (FSTDocumentKey *)value; +- (FSTDocumentKey *)valueWithOptions:(FSTFieldValueOptions *)options; @property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; @end /** * A structured object value stored in Firestore. */ -@interface FSTObjectValue : FSTFieldValue +// clang-format off +@interface FSTObjectValue : FSTFieldValue < NSDictionary<NSString *, id> * > + +- (instancetype)init NS_UNAVAILABLE; +// clang-format on + /** Returns an empty FSTObjectValue. */ + (instancetype)objectValue; @@ -198,9 +236,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { - (instancetype)initWithImmutableDictionary: (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; -- (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. */ @@ -222,19 +258,20 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { /** * An array value stored in Firestore. */ -@interface FSTArrayValue : FSTFieldValue +// clang-format off +@interface FSTArrayValue : FSTFieldValue < NSArray <id> * > + +- (instancetype)init NS_UNAVAILABLE; +// clang-format on /** * Initializes this instance with the given array of wrapped values. * * @param value An immutable array of FSTFieldValue objects. Caller is responsible for copying the - * value or releasing all references. + * value or releasing all references. */ - (instancetype)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -- (NSArray<id> *)value; - (NSArray<FSTFieldValue *> *)internalValue; @end diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m index 95ad306..a6326a7 100644 --- a/Firestore/Source/Model/FSTFieldValue.m +++ b/Firestore/Source/Model/FSTFieldValue.m @@ -17,6 +17,7 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/API/FIRGeoPoint+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Core/FSTTimestamp.h" #import "Firestore/Source/Model/FSTDatabaseID.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -27,6 +28,38 @@ NS_ASSUME_NONNULL_BEGIN +#pragma mark - FSTFieldValueOptions + +@implementation FSTFieldValueOptions + ++ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)options { + if (options.serverTimestampBehavior == FSTServerTimestampBehaviorNone) { + static FSTFieldValueOptions *defaultInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultInstance = [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + return defaultInstance; + } else { + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:options.serverTimestampBehavior]; + } +} + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + return self; +} + +@end + #pragma mark - FSTFieldValue @interface FSTFieldValue () @@ -40,6 +73,11 @@ NS_ASSUME_NONNULL_BEGIN } - (id)value { + return [self valueWithOptions:[FSTFieldValueOptions + optionsForSnapshotOptions:[FIRSnapshotOptions defaultOptions]]]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { @throw FSTAbstractMethodException(); // NOLINT } @@ -89,7 +127,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderNull; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return [NSNull null]; } @@ -155,7 +193,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBoolean; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue ? @YES : @NO; } @@ -233,7 +271,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -285,7 +323,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -332,7 +370,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderString; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -379,7 +417,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 +448,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 +468,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 FSTServerTimestampBehaviorNone: + 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 +531,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderGeoPoint; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -529,7 +579,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBlob; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -573,7 +623,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.key; } @@ -648,11 +698,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 +853,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..7c5f6de 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 @@ -196,16 +198,18 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * apply the transform if the prior mutation resulted in an FSTDocument (always true for an * FSTSetMutation, but not necessarily for an FSTPatchMutation). */ -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult; + mutationResult:(nullable FSTMutationResult *)mutationResult; /** * A helper version of applyTo for applying mutations locally (without a mutation result from the * backend). */ -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - localWriteTime:(FSTTimestamp *)localWriteTime; +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime; @property(nonatomic, strong, readonly) FSTDocumentKey *key; diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m index 5b47280..c249138 100644 --- a/Firestore/Source/Model/FSTMutation.m +++ b/Firestore/Source/Model/FSTMutation.m @@ -97,7 +97,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; FSTFieldTransform *otherFieldTransform = other; return [self.path isEqual:otherFieldTransform.path] && [self.transform isEqual:otherFieldTransform.transform]; @@ -236,15 +236,18 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { @throw FSTAbstractMethodException(); // NOLINT } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - localWriteTime:(FSTTimestamp *)localWriteTime { - return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil]; +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime { + return + [self applyTo:maybeDoc baseDocument:baseDoc localWriteTime:localWriteTime mutationResult:nil]; } @end @@ -287,9 +290,10 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); } @@ -362,9 +366,10 @@ NS_ASSUME_NONNULL_BEGIN self.key, self.fieldMask, self.value, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); } @@ -451,9 +456,10 @@ NS_ASSUME_NONNULL_BEGIN self.key, self.fieldTransforms, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(mutationResult.transformResults, @"Transform results missing for FSTTransformMutation."); @@ -473,8 +479,9 @@ NS_ASSUME_NONNULL_BEGIN BOOL hasLocalMutations = (mutationResult == nil); NSArray<FSTFieldValue *> *transformResults = - mutationResult ? mutationResult.transformResults - : [self localTransformResultsWithWriteTime:localWriteTime]; + mutationResult + ? mutationResult.transformResults + : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; return [FSTDocument documentWithData:newData key:doc.key @@ -486,16 +493,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 baseDocument 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 *> *)localTransformResultsWithBaseDocument: + (FSTMaybeDocument *_Nullable)baseDocument + 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 ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path]; + } + + [transformResults + addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]]; } else { FSTFail(@"Encountered unknown transform: %@", fieldTransform); } @@ -551,9 +568,10 @@ NS_ASSUME_NONNULL_BEGIN stringWithFormat:@"<FSTDeleteMutation key=%@ precondition=%@>", self.key, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTDeleteMutation."); diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m index 3677908..01adca7 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 + baseDocument:baseDoc localWriteTime:self.localWriteTime mutationResult:mutationResult]; } diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m index b236107..636c322 100644 --- a/Firestore/Source/Model/FSTPath.m +++ b/Firestore/Source/Model/FSTPath.m @@ -240,7 +240,7 @@ NS_ASSUME_NONNULL_BEGIN c = *source++; // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); - // Fall through + // Fall through default: // copy into the current segment diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h index 022c81b..4717067 100644 --- a/Firestore/Source/Public/FIRDocumentChange.h +++ b/Firestore/Source/Public/FIRDocumentChange.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN -@class FIRDocumentSnapshot; +@class FIRQueryDocumentSnapshot; /** An enumeration of document change types. */ typedef NS_ENUM(NSInteger, FIRDocumentChangeType) { @@ -47,7 +47,7 @@ NS_SWIFT_NAME(DocumentChange) @property(nonatomic, readonly) FIRDocumentChangeType type; /** The document affected by this change. */ -@property(nonatomic, strong, readonly) FIRDocumentSnapshot *document; +@property(nonatomic, strong, readonly) FIRQueryDocumentSnapshot *document; /** * The index of the changed document in the result set immediately prior to this FIRDocumentChange diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 439e727..7fcc7a8 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -36,8 +36,6 @@ NS_SWIFT_NAME(DocumentListenOptions) - (instancetype)init; -@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; - /** * Sets the includeMetadataChanges option which controls whether metadata-only changes (i.e. only * `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events. Default is NO. diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 3e67c25..6e79a7f 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -22,9 +22,61 @@ 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 `NSNull` for `FieldValue.serverTimestamp()` fields that have not yet + * been set to their final value. + */ + FIRServerTimestampBehaviorNone, + + /** + * 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 +} NS_SWIFT_NAME(ServerTimestampBehavior); + +/** + * 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)serverTimestampBehavior:(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. + * + * For a `FIRDocumentSnapshot` that points to a non-existing document, any data access will return + * `nil`. You can use the `exists` property to explicitly verify a documents existence. */ NS_SWIFT_NAME(DocumentSnapshot) @interface FIRDocumentSnapshot : NSObject @@ -46,21 +98,104 @@ NS_SWIFT_NAME(DocumentSnapshot) @property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; /** - * Retrieves all fields in the document as an `NSDictionary`. + * Retrieves all fields in the document as an `NSDictionary`. Returns `nil` if the document doesn't + * exist. * - * @return An `NSDictionary` containing all fields in the document. + * 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 or `nil` if the document doesn't + * exist. */ -- (NSDictionary<NSString *, id> *)data; +- (nullable NSDictionary<NSString *, id> *)data; + +/** + * Retrieves all fields in the document as a `Dictionary`. Returns `nil` if the document doesn't + * exist. + * + * @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 or `nil` if the document doesn't + * exist. + */ +- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options; + +/** + * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't + * exist. + * + * 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 document or field doesn't exist. + */ +- (nullable id)valueForField:(id)field NS_SWIFT_NAME(get(_:)); + +/** + * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't + * exist. + * + * 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 document or 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. * - * @return The value contained in the field or `nil` if the field doesn't exist. + * @return The value contained in the field or `nil` if the document or field doesn't exist. */ - (nullable id)objectForKeyedSubscript:(id)key; @end +/** + * A `FIRQueryDocumentSnapshot` contains data read from a document in your Firestore database as + * part of a query. The document is guaranteed to exist and its data can be extracted with the + * `data` property or by using subscript syntax to access a specific field. + * + * A `FIRQueryDocumentSnapshot` offers the same API surface as a `FIRDocumentSnapshot`. As + * deleted documents are not returned from queries, its `exists` property will always be true and + * `data:` will never return `nil`. + */ +NS_SWIFT_NAME(QueryDocumentSnapshot) +@interface FIRQueryDocumentSnapshot : FIRDocumentSnapshot + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRQueryDocumentSnapshot cannot be created directly."))); + +/** + * 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; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h index 91a96a5..4c85aba 100644 --- a/Firestore/Source/Public/FIRFirestore.h +++ b/Firestore/Source/Public/FIRFirestore.h @@ -139,6 +139,23 @@ NS_SWIFT_NAME(Firestore) + (void)enableLogging:(BOOL)logging DEPRECATED_MSG_ATTRIBUTE("Use FIRSetLoggerLevel(FIRLoggerLevelDebug) to enable logging"); +#pragma mark - Network + +/** + * Re-enables usage of the network by this Firestore instance after a prior call to + * `disableNetworkWithCompletion`. Completion block, if provided, will be called once network uasge + * has been enabled. + */ +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Disables usage of the network by this Firestore instance. It can be re-enabled by via + * `enableNetworkWithCompletion`. While the network is disabled, any snapshot listeners or get calls + * will return results from cache and any write operations will be queued until the network is + * restored. The completion block, if provided, will be called once network usage has been disabled. + */ +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index 0f3aeed..ff15ac6 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -256,6 +256,19 @@ NS_SWIFT_NAME(Query) isGreaterThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); // clang-format on +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * satisfy the specified predicate. + * + * @param predicate The predicate the document must satisfy. Can be either comparison + * or compound of comparison. In particular, block-based predicate is not supported. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate NS_SWIFT_NAME(filter(using:)); +// clang-format on + #pragma mark - Sorting Data /** * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h index c49a07a..1266832 100644 --- a/Firestore/Source/Public/FIRQuerySnapshot.h +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -19,8 +19,8 @@ NS_ASSUME_NONNULL_BEGIN @class FIRDocumentChange; -@class FIRDocumentSnapshot; @class FIRQuery; +@class FIRQueryDocumentSnapshot; @class FIRSnapshotMetadata; /** @@ -50,7 +50,7 @@ NS_SWIFT_NAME(QuerySnapshot) @property(nonatomic, readonly) NSInteger count; /** An Array of the `FIRDocumentSnapshots` that make up this document set. */ -@property(nonatomic, strong, readonly) NSArray<FIRDocumentSnapshot *> *documents; +@property(nonatomic, strong, readonly) NSArray<FIRQueryDocumentSnapshot *> *documents; /** * An array of the documents that changed since the last snapshot. If this is the first snapshot, diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h index 5f0034c..8ff1bec 100644 --- a/Firestore/Source/Public/FIRWriteBatch.h +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -94,6 +94,11 @@ NS_SWIFT_NAME(WriteBatch) /** * Commits all of the writes in this write batch as a single atomic unit. + */ +- (void)commit; + +/** + * Commits all of the writes in this write batch as a single atomic unit. * * @param completion A block to be called once all of the writes in the batch have been * successfully written to the backend as an atomic unit. This block will only execute @@ -101,7 +106,7 @@ NS_SWIFT_NAME(WriteBatch) * completion handler will not be called when the device is offline, though local * changes will be visible immediately. */ -- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion; +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 313ddb7..18331ff 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -83,7 +83,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol FSTOnlineStateDelegate <NSObject> /** Called whenever the online state of the watch stream changes */ -- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState; +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m index 063e487..a0c5059 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.m +++ b/Firestore/Source/Remote/FSTRemoteStore.m @@ -160,27 +160,38 @@ static const int kOnlineAttemptsBeforeFailure = 2; [self enableNetwork]; } -- (void)setOnlineStateToHealthy { - self.shouldWarnOffline = NO; - [self updateAndNotifyAboutOnlineState:FSTOnlineStateHealthy]; -} - -- (void)setOnlineStateToUnknown { - // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) or - // when we have no active listens and therefore there's no need to start the stream. Assuming - // there is (possibly in the future) an active listen, then we will eventually move to state - // Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure attempts - // before failing, so we reset the count here. - self.watchStreamFailures = 0; - [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; +/** + * Updates our OnlineState to the new state, updating local state and notifying the + * onlineStateHandler as appropriate. + */ +- (void)updateOnlineState:(FSTOnlineState)newState { + // Update and broadcast the new state. + if (newState != self.watchStreamOnlineState) { + if (newState == FSTOnlineStateHealthy) { + // We've connected to watch at least once. Don't warn the developer about being offline going + // forward. + self.shouldWarnOffline = NO; + } else if (newState == FSTOnlineStateUnknown) { + // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) + // or when we have no active listens and therefore there's no need to start the stream. + // Assuming there is (possibly in the future) an active listen, then we will eventually move + // to state Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure + // attempts before failing, so we reset the count here. + self.watchStreamFailures = 0; + } + self.watchStreamOnlineState = newState; + [self.onlineStateDelegate applyChangedOnlineState:newState]; + } } +/** + * Updates our FSTOnlineState as appropriate after the watch stream reports a failure. The first + * failure moves us to the 'Unknown' state. We then may allow multiple failures (based on + * kOnlineAttemptsBeforeFailure) before we actually transition to FSTOnlineStateFailed. + */ - (void)updateOnlineStateAfterFailure { - // The first failure after we are successfully connected moves us to the 'Unknown' state. We - // then may make multiple attempts (based on kOnlineAttemptsBeforeFailure) before we actually - // report failure. if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } else { self.watchStreamFailures++; if (self.watchStreamFailures >= kOnlineAttemptsBeforeFailure) { @@ -188,19 +199,11 @@ static const int kOnlineAttemptsBeforeFailure = 2; FSTWarn(@"Could not reach Firestore backend."); self.shouldWarnOffline = NO; } - [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + [self updateOnlineState:FSTOnlineStateFailed]; } } } -- (void)updateAndNotifyAboutOnlineState:(FSTOnlineState)watchStreamOnlineState { - BOOL didChange = (watchStreamOnlineState != self.watchStreamOnlineState); - self.watchStreamOnlineState = watchStreamOnlineState; - if (didChange) { - [self.onlineStateDelegate watchStreamDidChangeOnlineState:watchStreamOnlineState]; - } -} - #pragma mark Online/Offline state - (BOOL)isNetworkEnabled { @@ -210,8 +213,9 @@ static const int kOnlineAttemptsBeforeFailure = 2; } - (void)enableNetwork { - FSTAssert(self.watchStream == nil, @"enableNetwork: called with non-null watchStream."); - FSTAssert(self.writeStream == nil, @"enableNetwork: called with non-null writeStream."); + if ([self isNetworkEnabled]) { + return; + } // Create new streams (but note they're not started yet). self.watchStream = [self.datastore createWatchStream]; @@ -227,47 +231,51 @@ static const int kOnlineAttemptsBeforeFailure = 2; [self fillWritePipeline]; // This may start the writeStream. // We move back to the unknown state because we might not want to re-open the stream - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } - (void)disableNetwork { - [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + [self disableNetworkInternal]; + // Set the FSTOnlineState to failed so get()'s return from cache, etc. + [self updateOnlineState:FSTOnlineStateFailed]; +} - // NOTE: We're guaranteed not to get any further events from these streams (not even a close - // event). - [self.watchStream stop]; - [self.writeStream stop]; +/** Disables the network, setting the FSTOnlineState to the specified targetOnlineState. */ +- (void)disableNetworkInternal { + if ([self isNetworkEnabled]) { + // NOTE: We're guaranteed not to get any further events from these streams (not even a close + // event). + [self.watchStream stop]; + [self.writeStream stop]; - [self cleanUpWatchStreamState]; - [self cleanUpWriteStreamState]; + [self cleanUpWatchStreamState]; + [self cleanUpWriteStreamState]; - self.writeStream = nil; - self.watchStream = nil; + self.writeStream = nil; + self.watchStream = nil; + } } #pragma mark Shutdown - (void)shutdown { FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); - - // Don't fire initial listener callbacks on shutdown. - self.onlineStateDelegate = nil; - - // For now, all shutdown logic is handled by disableNetwork(). We might expand on this in the - // future. - if ([self isNetworkEnabled]) { - [self disableNetwork]; - } + [self disableNetworkInternal]; + // Set the FSTOnlineState to Unknown (rather than Failed) to avoid potentially triggering + // spurious listener events with cached data, etc. + [self updateOnlineState:FSTOnlineStateUnknown]; } - (void)userDidChange:(FSTUser *)user { FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); - - // Tear down and re-create our network streams. This will ensure we get a fresh auth token - // for the new user and re-fill the write pipeline with new mutations from the LocalStore - // (since mutations are per-user). - [self disableNetwork]; - [self enableNetwork]; + if ([self isNetworkEnabled]) { + // Tear down and re-create our network streams. This will ensure we get a fresh auth token + // for the new user and re-fill the write pipeline with new mutations from the LocalStore + // (since mutations are per-user). + [self disableNetworkInternal]; + [self updateOnlineState:FSTOnlineStateUnknown]; + [self enableNetwork]; + } } #pragma mark Watch Stream @@ -348,7 +356,7 @@ static const int kOnlineAttemptsBeforeFailure = 2; - (void)watchStreamDidChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { // Mark the connection as healthy because we got a message from the server. - [self setOnlineStateToHealthy]; + [self updateOnlineState:FSTOnlineStateHealthy]; FSTWatchTargetChange *watchTargetChange = [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; @@ -391,7 +399,7 @@ static const int kOnlineAttemptsBeforeFailure = 2; } else { // We don't need to restart the watch stream because there are no active targets. The online // state is set to unknown because there is no active attempt at establishing a connection. - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } } @@ -532,6 +540,8 @@ static const int kOnlineAttemptsBeforeFailure = 2; - (void)cleanUpWriteStreamState { self.lastBatchSeen = kFSTBatchIDUnknown; + FSTLog(@"Stopping write stream with %lu pending writes", + (unsigned long)[self.pendingWrites count]); [self.pendingWrites removeAllObjects]; } diff --git a/Firestore/Source/Remote/FSTStream.m b/Firestore/Source/Remote/FSTStream.m index 2c039be..5719ec8 100644 --- a/Firestore/Source/Remote/FSTStream.m +++ b/Firestore/Source/Remote/FSTStream.m @@ -542,7 +542,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; FSTWeakify(self); [self.workerDispatchQueue dispatchAsync:^{ FSTStrongify(self); - if (!self || ![self isStarted]) { + if (![self isStarted]) { FSTLog(@"%@ Ignoring stream message from inactive stream.", NSStringFromClass([self class])); } diff --git a/Firestore/core/src/firebase/firestore/util/assert_apple.mm b/Firestore/core/src/firebase/firestore/util/assert_apple.mm index 0447d6c..83b76e1 100644 --- a/Firestore/core/src/firebase/firestore/util/assert_apple.mm +++ b/Firestore/core/src/firebase/firestore/util/assert_apple.mm @@ -26,16 +26,23 @@ namespace firebase { namespace firestore { namespace util { -void FailAssert(const char* file, const char* func, const int line, const char* format, ...) { +void FailAssert(const char* file, + const char* func, + const int line, + const char* format, + ...) { va_list args; va_start(args, format); - NSString *description = [[NSString alloc] initWithFormat:WrapNSStringNoCopy(format) arguments:args]; + NSString* description = + [[NSString alloc] initWithFormat:WrapNSStringNoCopy(format) + arguments:args]; va_end(args); [[NSAssertionHandler currentHandler] handleFailureInFunction:WrapNSStringNoCopy(func) file:WrapNSStringNoCopy(file) lineNumber:line - description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", description]; + description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", + description]; abort(); } diff --git a/Firestore/core/src/firebase/firestore/util/assert_stdio.cc b/Firestore/core/src/firebase/firestore/util/assert_stdio.cc index b5d0b7c..5476e65 100644 --- a/Firestore/core/src/firebase/firestore/util/assert_stdio.cc +++ b/Firestore/core/src/firebase/firestore/util/assert_stdio.cc @@ -29,8 +29,11 @@ namespace firebase { namespace firestore { namespace util { -void FailAssert(const char* file, const char* func, const int line, - const char* format, ...) { +void FailAssert(const char* file, + const char* func, + const int line, + const char* format, + ...) { std::string message; StringAppendF(&message, "ASSERT: %s(%d) %s: ", file, line, func); diff --git a/Firestore/core/src/firebase/firestore/util/firebase_assert.h b/Firestore/core/src/firebase/firestore/util/firebase_assert.h index 9f1bce8..da01864 100644 --- a/Firestore/core/src/firebase/firestore/util/firebase_assert.h +++ b/Firestore/core/src/firebase/firestore/util/firebase_assert.h @@ -35,14 +35,14 @@ // Assert condition is true, if it's false log an assert with the specified // expression as a string. -#define FIREBASE_ASSERT_WITH_EXPRESSION(condition, expression) \ - do { \ - if (!(condition)) { \ - firebase::firestore::util::FailAssert( \ - __FILE__, __PRETTY_FUNCTION__, __LINE__, \ - FIREBASE_EXPAND_STRINGIFY(expression)); \ - } \ - } while(0) +#define FIREBASE_ASSERT_WITH_EXPRESSION(condition, expression) \ + do { \ + if (!(condition)) { \ + firebase::firestore::util::FailAssert( \ + __FILE__, __PRETTY_FUNCTION__, __LINE__, \ + FIREBASE_EXPAND_STRINGIFY(expression)); \ + } \ + } while (0) // Assert condition is true, if it's false log an assert with the specified // expression as a string. Compiled out of release builds. @@ -65,15 +65,15 @@ // Assert condition is true otherwise display the specified expression, // message and abort. -#define FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION(condition, expression, ...) \ - do { \ - if (!(condition)) { \ - firebase::firestore::util::LogError( \ - FIREBASE_EXPAND_STRINGIFY(expression)); \ - firebase::firestore::util::FailAssert( \ - __FILE__, __PRETTY_FUNCTION__, __LINE__, __VA_ARGS__); \ - } \ - } while(0) +#define FIREBASE_ASSERT_MESSAGE_WITH_EXPRESSION(condition, expression, ...) \ + do { \ + if (!(condition)) { \ + firebase::firestore::util::LogError( \ + FIREBASE_EXPAND_STRINGIFY(expression)); \ + firebase::firestore::util::FailAssert(__FILE__, __PRETTY_FUNCTION__, \ + __LINE__, __VA_ARGS__); \ + } \ + } while (0) // Assert condition is true otherwise display the specified expression, // message and abort. Compiled out of release builds. @@ -92,8 +92,11 @@ namespace firestore { namespace util { // A no-return helper function. To raise an assertion, use Macro instead. -void FailAssert(const char* file, const char* func, const int line, - const char* format, ...); +void FailAssert(const char* file, + const char* func, + const int line, + const char* format, + ...); } // namespace util } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/util/string_apple.h b/Firestore/core/src/firebase/firestore/util/string_apple.h index 42b51dd..e1be8c3 100644 --- a/Firestore/core/src/firebase/firestore/util/string_apple.h +++ b/Firestore/core/src/firebase/firestore/util/string_apple.h @@ -25,13 +25,13 @@ namespace util { // Translates a C string to the equivalent NSString without making a copy. inline NSString* WrapNSStringNoCopy(const char* c_str) { - return [[NSString alloc] initWithBytesNoCopy:const_cast<void*>(static_cast<const void*>(c_str)) - length:strlen(c_str) - encoding:NSUTF8StringEncoding - freeWhenDone:NO]; + return [[NSString alloc] + initWithBytesNoCopy:const_cast<void*>(static_cast<const void*>(c_str)) + length:strlen(c_str) + encoding:NSUTF8StringEncoding + freeWhenDone:NO]; } - } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/string_printf.h b/Firestore/core/src/firebase/firestore/util/string_printf.h index 9e2b9c0..d15296e 100644 --- a/Firestore/core/src/firebase/firestore/util/string_printf.h +++ b/Firestore/core/src/firebase/firestore/util/string_printf.h @@ -28,8 +28,7 @@ namespace firestore { namespace util { /** Return a C++ string. */ -std::string StringPrintf(const char* format, ...) - ABSL_PRINTF_ATTRIBUTE(1, 2); +std::string StringPrintf(const char* format, ...) ABSL_PRINTF_ATTRIBUTE(1, 2); /** Append result to a supplied string. */ void StringAppendF(std::string* dst, const char* format, ...) |