diff options
author | Sebastian Schmidt <mrschmidt@google.com> | 2018-05-02 11:10:19 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-02 11:10:19 -0700 |
commit | 542d81ac68c416e8d76839e438ad1d6aaab528f3 (patch) | |
tree | d9cdc0797d3757d6899047f9f78b578b73fd2cdd | |
parent | 39e68afc1a76f5e2ee19405bd32de7b335d4fb43 (diff) |
Adding mergeFields support (#1141)
-rw-r--r-- | Firestore/CHANGELOG.md | 2 | ||||
-rw-r--r-- | Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm | 149 | ||||
-rw-r--r-- | Firestore/Source/API/FIRDocumentReference.mm | 20 | ||||
-rw-r--r-- | Firestore/Source/API/FIRTransaction.mm | 15 | ||||
-rw-r--r-- | Firestore/Source/API/FIRWriteBatch.mm | 17 | ||||
-rw-r--r-- | Firestore/Source/API/FSTUserDataConverter.h | 2 | ||||
-rw-r--r-- | Firestore/Source/API/FSTUserDataConverter.mm | 42 | ||||
-rw-r--r-- | Firestore/Source/Public/FIRDocumentReference.h | 39 | ||||
-rw-r--r-- | Firestore/Source/Public/FIRTransaction.h | 24 | ||||
-rw-r--r-- | Firestore/Source/Public/FIRWriteBatch.h | 23 | ||||
-rw-r--r-- | Firestore/core/src/firebase/firestore/model/field_mask.h | 16 |
11 files changed, 339 insertions, 10 deletions
diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 5543325..c97aa23 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -24,6 +24,8 @@ Query.getDocuments() should fetch from server only, cache only, or attempt server and fall back to the cache (which was the only option previously, and is now the default.) +- [feature] Added new `mergeFields:(NSArray<id>*)` override for `set()` + which allows merging of a reduced subset of fields. # v0.11.0 - [fixed] Fixed a regression in the Firebase iOS SDK release 4.11.0 that could diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index 9b6febe..f8091c0 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -256,6 +256,155 @@ XCTAssertEqualObjects(document.data, finalData); } +- (void)testCannotSpecifyFieldMaskForMissingField { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTAssertThrowsSpecific( + { [doc setData:@{} mergeFields:@[ @"foo" ]]; }, NSException, + @"Field 'foo' is specified in your field mask but missing from your input data."); +} + +- (void)testCanSetASubsetOfFieldsUsingMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : @"NewDescription", @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldDeleteOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForDelete], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldTransformOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSetEmptyFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = initialData; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSpecifyFieldsMultipleTimesInFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{ + @"desc" : @"Description", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{ + @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + } + mergeFields:@[ @"owner.name", @"owner", @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + - (void)testAddingToACollectionYieldsTheCorrectDocumentReference { FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index c2fc546..da67a5b 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -125,6 +125,11 @@ NS_ASSUME_NONNULL_BEGIN } - (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields { + return [self setData:documentData mergeFields:mergeFields completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { return [self setData:documentData merge:NO completion:completion]; } @@ -132,8 +137,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:documentData] - : [self.firestore.dataConverter parsedSetData:documentData]; + FSTParsedSetData *parsed = + merge ? [self.firestore.dataConverter parsedMergeData:documentData fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:documentData]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] + completion:completion]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:documentData fieldMask:mergeFields]; return [self.firestore.client writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] completion:completion]; diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm index 668a359..b5bdefa 100644 --- a/Firestore/Source/API/FIRTransaction.mm +++ b/Firestore/Source/API/FIRTransaction.mm @@ -68,8 +68,19 @@ NS_ASSUME_NONNULL_BEGIN forDocument:(FIRDocumentReference *)document merge:(BOOL)merge { [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.internalTransaction setData:parsed forDocument:document.key]; return self; } diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm index 1185dae..366c708 100644 --- a/Firestore/Source/API/FIRWriteBatch.mm +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -70,8 +70,21 @@ NS_ASSUME_NONNULL_BEGIN merge:(BOOL)merge { [self verifyNotCommitted]; [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; return self; diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h index a3d8a2d..27a5f09 100644 --- a/Firestore/Source/API/FSTUserDataConverter.h +++ b/Firestore/Source/API/FSTUserDataConverter.h @@ -129,7 +129,7 @@ typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable); - (FSTParsedSetData *)parsedSetData:(id)input; /** Parse document data from a setData call with `merge:YES`. */ -- (FSTParsedSetData *)parsedMergeData:(id)input; +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask; /** Parse update data from an updateData call. */ - (FSTParsedUpdateData *)parsedUpdateData:(id)input; diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 2794398..6d01c75 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -412,7 +412,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { return self; } -- (FSTParsedSetData *)parsedMergeData:(id)input { +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask { // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust // Obj-C to verify the type for us. if (![input isKindOfClass:[NSDictionary class]]) { @@ -424,9 +424,45 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { path:absl::make_unique<FieldPath>(FieldPath::EmptyPath())]; FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + FieldMask convertedFieldMask; + std::vector<FieldTransform> convertedFieldTransform; + + if (fieldMask) { + __block std::vector<FieldPath> fieldMaskPaths{}; + [fieldMask enumerateObjectsUsingBlock:^(id fieldPath, NSUInteger idx, BOOL *stop) { + FieldPath path{}; + + if ([fieldPath isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:fieldPath].internalValue; + } else if ([fieldPath isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)fieldPath).internalValue; + } else { + FSTThrowInvalidArgument( + @"All elements in mergeFields: must be NSStrings or FIRFieldPaths."); + } + + if ([updateData valueForPath:path] == nil) { + FSTThrowInvalidArgument( + @"Field '%s' is specified in your field mask but missing from your input data.", + path.CanonicalString().c_str()); + } + + fieldMaskPaths.push_back(path); + }]; + convertedFieldMask = FieldMask(fieldMaskPaths); + std::copy_if(context.fieldTransforms->begin(), context.fieldTransforms->end(), + std::back_inserter(convertedFieldTransform), + [&](const FieldTransform &fieldTransform) { + return convertedFieldMask.covers(fieldTransform.path()); + }); + } else { + convertedFieldMask = FieldMask{*context.fieldMask}; + convertedFieldTransform = *context.fieldTransforms; + } + return [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:FieldMask{*context.fieldMask} - fieldTransforms:*context.fieldTransforms]; + fieldMask:convertedFieldMask + fieldTransforms:convertedFieldTransform]; } - (FSTParsedSetData *)parsedSetData:(id)input { diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 4aa8c45..7baa30a 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -92,6 +92,23 @@ NS_SWIFT_NAME(DocumentReference) - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields; + +/** * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it * is created. If a document already exists, it is overwritten. * @@ -121,6 +138,28 @@ NS_SWIFT_NAME(DocumentReference) completion:(nullable void (^)(NSError *_Nullable error))completion; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @param completion A block to execute once the document has been successfully written to the + * server. This block will not be called while the client is offline, though local + * changes will be visible immediately. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** * Updates fields in the document referred to by this `FIRDocumentReference`. * If the document does not exist, the update fails (specify a completion block to be notified). * diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h index 2fa4430..e53414d 100644 --- a/Firestore/Source/Public/FIRTransaction.h +++ b/Firestore/Source/Public/FIRTransaction.h @@ -65,6 +65,30 @@ NS_SWIFT_NAME(Transaction) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param data An `NSDictionary` containing the fields that make up the document + * to be written. + * @param document A reference to the document whose data should be overwritten. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If the document does not exist, the transaction will fail. * diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h index 1568723..22d1b16 100644 --- a/Firestore/Source/Public/FIRWriteBatch.h +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -68,6 +68,29 @@ NS_SWIFT_NAME(WriteBatch) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If document does not exist, the write batch will fail. * diff --git a/Firestore/core/src/firebase/firestore/model/field_mask.h b/Firestore/core/src/firebase/firestore/model/field_mask.h index b895ab3..6075519 100644 --- a/Firestore/core/src/firebase/firestore/model/field_mask.h +++ b/Firestore/core/src/firebase/firestore/model/field_mask.h @@ -55,6 +55,22 @@ class FieldMask { return fields_.end(); } + /** + * Verifies that `fieldPath` is included by at least one field in this field + * mask. + * + * This is an O(n) operation, where `n` is the size of the field mask. + */ + bool covers(const FieldPath& fieldPath) const { + for (const FieldPath& fieldMaskPath : fields_) { + if (fieldMaskPath.IsPrefixOf(fieldPath)) { + return true; + } + } + + return false; + } + std::string ToString() const { // Ideally, one should use a string builder. Since this is only non-critical // code for logging and debugging, the logic is kept simple here. |