diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index b8de1e9eab5..8daae9184d6 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -5,6 +5,9 @@ - [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. # v0.9.4 - [changed] Firestore no longer has a direct dependency on FirebaseAuth. diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m index 087eb0130fb..1c31242615a 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m @@ -83,6 +83,13 @@ - (void)testDeleteDocument { 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]; diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m index 970dc90b36e..d1d9999591e 100644 --- a/Firestore/Source/API/FIRDocumentChange.m +++ b/Firestore/Source/API/FIRDocumentChange.m @@ -57,11 +57,11 @@ + (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)ch NSUInteger index = 0; NSMutableArray *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 @@ + (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)ch FSTDocumentSet *indexTracker = snapshot.oldDocuments; NSMutableArray *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 @@ + (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)ch @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/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m index c4e30402104..36430ac033b 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -26,6 +26,7 @@ #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 @@ -100,23 +101,15 @@ - (FIRSnapshotMetadata *)metadata { return _cachedMetadata; } -- (NSDictionary *)data { +- (nullable NSDictionary *)data { return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; } -- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { - 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); - } - - return [self convertedObject:[self.internalDocument data] - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; +- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options { + return self.internalDocument == nil + ? nil + : [self convertedObject:[self.internalDocument data] + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; } - (nullable id)valueForField:(id)field { @@ -135,8 +128,10 @@ - (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { } FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return [self convertedValue:fieldValue - options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; + return fieldValue == nil + ? nil + : [self convertedValue:fieldValue + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; } - (nullable id)objectForKeyedSubscript:(id)key { @@ -190,4 +185,34 @@ - (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)opti @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 *)data { + NSDictionary *data = [super data]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m index 6bc6761970b..5e1af9a5b01 100644 --- a/Firestore/Source/API/FIRQuerySnapshot.m +++ b/Firestore/Source/API/FIRQuerySnapshot.m @@ -57,7 +57,7 @@ + (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore @implementation FIRQuerySnapshot { // Cached value of the documents property. - NSArray *_documents; + NSArray *_documents; // Cached value of the documentChanges property. NSArray *_documentChanges; @@ -93,18 +93,18 @@ - (NSInteger)count { return self.snapshot.documents.count; } -- (NSArray *)documents { +- (NSArray *)documents { if (!_documents) { FSTDocumentSet *documentSet = self.snapshot.documents; FIRFirestore *firestore = self.firestore; BOOL fromCache = self.metadata.fromCache; - NSMutableArray *result = [NSMutableArray array]; + NSMutableArray *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/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h index 022c81bd476..47170673068 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/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 994cabf680c..78ab2e5e2fb 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -74,6 +74,9 @@ NS_SWIFT_NAME(SnapshotOptions) * 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 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 @@ -95,50 +98,51 @@ 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. * - * 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. + * 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. + * @return An `NSDictionary` containing all fields in the document or `nil` if the document doesn't + * exist. */ -- (NSDictionary *)data; +- (nullable NSDictionary *)data; /** - * Retrieves all fields in the document as a `Dictionary`. + * 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. + * @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. */ -- (NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options; +- (nullable NSDictionary *)dataWithOptions:(FIRSnapshotOptions *)options; /** - * Retrieves a specific field from the document. + * 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. + * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The + * can use `get(_:options:)` to configure this behavior. * * @param field The field to retrieve. - * @return The value contained in the field or `nil` if the field doesn't exist. + * @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. + * 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. + * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The + * can use `get(_:options:)` to configure this behavior. * * @param field The field to retrieve. - * @param options `SnapshotOptions` to configure how data is returned from the - * snapshot (e.g. the desired behavior for server timestamps that have not - * yet been set to their final value). - * @return The value contained in the field or `nil` if the field doesn't exist. + * @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 @@ -151,10 +155,35 @@ NS_SWIFT_NAME(DocumentSnapshot) * * @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`. + * + * @return An `NSDictionary` containing all fields in the document. + */ +- (NSDictionary *)data; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h index c49a07ad8c8..12668324e8b 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 *documents; +@property(nonatomic, strong, readonly) NSArray *documents; /** * An array of the documents that changed since the last snapshot. If this is the first snapshot,