diff options
-rw-r--r-- | Firestore/Example/SwiftBuildTest/main.swift | 4 | ||||
-rw-r--r-- | Firestore/Example/Tests/Model/FSTMutationTests.mm | 62 | ||||
-rw-r--r-- | Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm | 6 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTHelpers.h | 8 | ||||
-rw-r--r-- | Firestore/Example/Tests/Util/FSTHelpers.mm | 24 | ||||
-rw-r--r-- | Firestore/Source/API/FIRFieldValue+Internal.h | 12 | ||||
-rw-r--r-- | Firestore/Source/API/FIRFieldValue.mm | 38 | ||||
-rw-r--r-- | Firestore/Source/API/FSTUserDataConverter.mm | 45 | ||||
-rw-r--r-- | Firestore/Source/Public/FIRFieldValue.h | 23 | ||||
-rw-r--r-- | Firestore/core/src/firebase/firestore/model/transform_operations.h | 67 | ||||
-rw-r--r-- | Firestore/core/test/firebase/firestore/model/transform_operations_test.cc | 3 |
11 files changed, 272 insertions, 20 deletions
diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift index 3087085..691adf6 100644 --- a/Firestore/Example/SwiftBuildTest/main.swift +++ b/Firestore/Example/SwiftBuildTest/main.swift @@ -100,6 +100,10 @@ func writeDocument(at docRef: DocumentReference) { let updateData = [ "bar.baz": 42, FieldPath(["foobar"]): 42, + "server_timestamp": FieldValue.serverTimestamp(), + "array_union": FieldValue.arrayUnion(["a", "b"]), + "array_remove": FieldValue.arrayRemove(["a", "b"]), + "field_delete": FieldValue.delete(), ] as [AnyHashable: Any] docRef.setData(setData) diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm index 60cb7b8..56bf1c2 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.mm +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -16,9 +16,12 @@ #import "Firestore/Source/Model/FSTMutation.h" +#import <FirebaseFirestore/FIRFieldValue.h> #import <FirebaseFirestore/FIRTimestamp.h> #import <XCTest/XCTest.h> +#include <vector> + #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" @@ -26,13 +29,19 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" +#include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/transform_operations.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" namespace testutil = firebase::firestore::testutil; +using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; +using firebase::firestore::model::FieldPath; +using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; +using firebase::firestore::model::TransformOperation; @interface FSTMutationTests : XCTestCase @end @@ -104,11 +113,12 @@ using firebase::firestore::model::Precondition; XCTAssertEqualObjects(patchedDoc, baseDoc); } -- (void)testAppliesLocalTransformsToDocuments { +- (void)testAppliesLocalServerTimestampTransformToDocuments { NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, NO); - FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + FSTMutation *transform = FSTTestTransformMutation( + @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp]; @@ -130,11 +140,57 @@ using firebase::firestore::model::Precondition; XCTAssertEqualObjects(transformedDoc, expectedDoc); } +// NOTE: This is more a test of FSTUserDataConverter code than FSTMutation code but we don't have +// unit tests for it currently. We could consider removing this test once we have integration tests. +- (void)testCreateArrayUnionTransform { + FSTTransformMutation *transform = FSTTestTransformMutation(@"collection/key", @{ + @"foo" : [FIRFieldValue fieldValueForArrayUnion:@[ @"tag" ]], + @"bar.baz" : [FIRFieldValue fieldValueForArrayUnion:@[ @YES, @[ @1, @2 ], @{@"a" : @"b"} ]] + }); + XCTAssertEqual(transform.fieldTransforms.size(), 2); + + const FieldTransform &first = transform.fieldTransforms[0]; + XCTAssertEqual(first.path(), FieldPath({"foo"})); + { + std::vector<FSTFieldValue *> expectedElements{FSTTestFieldValue(@"tag")}; + ArrayTransform expected(TransformOperation::Type::ArrayUnion, expectedElements); + XCTAssertEqual(static_cast<const ArrayTransform &>(first.transformation()), expected); + } + + const FieldTransform &second = transform.fieldTransforms[1]; + XCTAssertEqual(second.path(), FieldPath({"bar", "baz"})); + { + std::vector<FSTFieldValue *> expectedElements { + FSTTestFieldValue(@YES), FSTTestFieldValue(@[ @1, @2 ]), FSTTestFieldValue(@{@"a" : @"b"}) + }; + ArrayTransform expected(TransformOperation::Type::ArrayUnion, expectedElements); + XCTAssertEqual(static_cast<const ArrayTransform &>(second.transformation()), expected); + } +} + +// NOTE: This is more a test of FSTUserDataConverter code than FSTMutation code but we don't have +// unit tests for it currently. We could consider removing this test once we have integration tests. +- (void)testCreateArrayRemoveTransform { + FSTTransformMutation *transform = FSTTestTransformMutation(@"collection/key", @{ + @"foo" : [FIRFieldValue fieldValueForArrayRemove:@[ @"tag" ]], + }); + XCTAssertEqual(transform.fieldTransforms.size(), 1); + + const FieldTransform &first = transform.fieldTransforms[0]; + XCTAssertEqual(first.path(), FieldPath({"foo"})); + { + std::vector<FSTFieldValue *> expectedElements{FSTTestFieldValue(@"tag")}; + const ArrayTransform expected(TransformOperation::Type::ArrayRemove, expectedElements); + XCTAssertEqual(static_cast<const ArrayTransform &>(first.transformation()), expected); + } +} + - (void)testAppliesServerAckedTransformsToDocuments { NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" }; FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, NO); - FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]); + FSTMutation *transform = FSTTestTransformMutation( + @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(1) diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm index 454b108..a648cd8 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -17,6 +17,7 @@ #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import <FirebaseFirestore/FIRFieldPath.h> +#import <FirebaseFirestore/FIRFieldValue.h> #import <FirebaseFirestore/FIRFirestoreErrors.h> #import <FirebaseFirestore/FIRGeoPoint.h> #import <FirebaseFirestore/FIRTimestamp.h> @@ -367,7 +368,10 @@ NS_ASSUME_NONNULL_BEGIN } - (void)testEncodesTransformMutation { - FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]); + FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @{ + @"a" : [FIRFieldValue fieldValueForServerTimestamp], + @"bar.baz" : [FIRFieldValue fieldValueForServerTimestamp] + }); GCFSWrite *proto = [GCFSWrite message]; proto.transform = [GCFSDocumentTransform message]; proto.transform.document = [self.serializer encodedDocumentKey:mutation.key]; diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index d05c1f1..9b5f96a 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -245,8 +245,12 @@ FSTPatchMutation *FSTTestPatchMutation( NSDictionary<NSString *, id> *values, const std::vector<firebase::firestore::model::FieldPath> &updateMask); -FSTTransformMutation *FSTTestTransformMutation(NSString *path, - NSArray<NSString *> *serverTimestampFields); +/** + * Creates a FSTTransformMutation by parsing any FIRFieldValue sentinels in the provided data. The + * data is expected to use dotted-notation for nested fields (i.e. + * @{ @"foo.bar": [FIRFieldValue ...] } and must not contain any non-sentinel data. + */ +FSTTransformMutation *FSTTestTransformMutation(NSString *path, NSDictionary<NSString *, id> *data); /** Creates a delete mutation for the document key at the given path. */ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 78b41a4..e1ed18c 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -122,7 +122,7 @@ NSDateComponents *FSTTestDateComponents( return comps; } -FSTFieldValue *FSTTestFieldValue(id _Nullable value) { +FSTUserDataConverter *FSTTestUserDataConverter() { // This owns the DatabaseIds since we do not have FirestoreClient instance to own them. static DatabaseId database_id{"project", DatabaseId::kDefault}; FSTUserDataConverter *converter = @@ -130,6 +130,11 @@ FSTFieldValue *FSTTestFieldValue(id _Nullable value) { preConverter:^id _Nullable(id _Nullable input) { return input; }]; + return converter; +} + +FSTFieldValue *FSTTestFieldValue(id _Nullable value) { + FSTUserDataConverter *converter = FSTTestUserDataConverter(); // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent. return [converter parsedQueryValue:value]; @@ -279,17 +284,14 @@ FSTPatchMutation *FSTTestPatchMutation(const absl::string_view path, precondition:Precondition::Exists(true)]; } -// For now this only creates TransformMutations with server timestamps. -FSTTransformMutation *FSTTestTransformMutation(NSString *path, - NSArray<NSString *> *serverTimestampFields) { +FSTTransformMutation *FSTTestTransformMutation(NSString *path, NSDictionary<NSString *, id> *data) { FSTDocumentKey *key = [FSTDocumentKey keyWithPath:testutil::Resource(util::MakeStringView(path))]; - std::vector<FieldTransform> fieldTransforms; - for (NSString *field in serverTimestampFields) { - FieldPath fieldPath = testutil::Field(util::MakeStringView(field)); - auto transformOp = absl::make_unique<ServerTimestampTransform>(ServerTimestampTransform::Get()); - fieldTransforms.emplace_back(std::move(fieldPath), std::move(transformOp)); - } - return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:std::move(fieldTransforms)]; + FSTUserDataConverter *converter = FSTTestUserDataConverter(); + FSTParsedUpdateData *result = [converter parsedUpdateData:data]; + FSTCAssert(result.data.value.count == 0, + @"FSTTestTransformMutation() only expects transforms; no other data"); + return [[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:std::move(result.fieldTransforms)]; } FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) { diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h index 2eb927e..1618cd4 100644 --- a/Firestore/Source/API/FIRFieldValue+Internal.h +++ b/Firestore/Source/API/FIRFieldValue+Internal.h @@ -42,4 +42,16 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; @end +/** FIRFieldValue class for array unions. */ +@interface FSTArrayUnionFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@property(strong, nonatomic, readonly) NSArray<id> *elements; +@end + +/** FIRFieldValue class for array removes. */ +@interface FSTArrayRemoveFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@property(strong, nonatomic, readonly) NSArray<id> *elements; +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.mm b/Firestore/Source/API/FIRFieldValue.mm index 0d0a9c4..e0ed8c7 100644 --- a/Firestore/Source/API/FIRFieldValue.mm +++ b/Firestore/Source/API/FIRFieldValue.mm @@ -82,6 +82,36 @@ NS_ASSUME_NONNULL_BEGIN @end +#pragma mark - FSTArrayUnionFieldValue + +@interface FSTArrayUnionFieldValue () +- (instancetype)initWithElements:(NSArray<id> *)elements; +@end + +@implementation FSTArrayUnionFieldValue +- (instancetype)initWithElements:(NSArray<id> *)elements { + if (self = [super initPrivate]) { + _elements = elements; + } + return self; +} +@end + +#pragma mark - FSTArrayRemoveFieldValue + +@interface FSTArrayRemoveFieldValue () +- (instancetype)initWithElements:(NSArray<id> *)elements; +@end + +@implementation FSTArrayRemoveFieldValue +- (instancetype)initWithElements:(NSArray<id> *)elements { + if (self = [super initPrivate]) { + _elements = elements; + } + return self; +} +@end + #pragma mark - FIRFieldValue @implementation FIRFieldValue @@ -99,6 +129,14 @@ NS_ASSUME_NONNULL_BEGIN return [FSTServerTimestampFieldValue serverTimestampFieldValue]; } ++ (instancetype)fieldValueForArrayUnion:(NSArray<id> *)elements { + return [[FSTArrayUnionFieldValue alloc] initWithElements:elements]; +} + ++ (instancetype)fieldValueForArrayRemove:(NSArray<id> *)elements { + return [[FSTArrayRemoveFieldValue alloc] initWithElements:elements]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index dff9235..90d68d8 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -44,6 +44,7 @@ #include "absl/memory/memory.h" namespace util = firebase::firestore::util; +using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; @@ -165,7 +166,11 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { FSTUserDataSourceSet, FSTUserDataSourceMergeSet, FSTUserDataSourceUpdate, - FSTUserDataSourceQueryValue, // from a where clause or cursor bound. + /** + * Indicates the source is a where clause, cursor bound, arrayUnion() element, etc. In particular, + * this will result in [FSTParseContext isWrite] returning NO. + */ + FSTUserDataSourceArgument, }; #pragma mark - FSTParseContext @@ -318,7 +323,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { case FSTUserDataSourceMergeSet: // Falls through. case FSTUserDataSourceUpdate: return YES; - case FSTUserDataSourceQueryValue: + case FSTUserDataSourceArgument: return NO; default: FSTThrowInvalidArgument(@"Unexpected case for FSTUserDataSource: %d", self.dataSource); @@ -489,7 +494,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { - (FSTFieldValue *)parsedQueryValue:(id)input { FSTParseContext *context = - [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue + [FSTParseContext contextWithSource:FSTUserDataSourceArgument path:absl::make_unique<FieldPath>(FieldPath::EmptyPath())]; FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; FSTAssert(parsed, @"Parsed data should not be nil."); @@ -593,10 +598,28 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { @"SetOptions.merge()%@", [context fieldDescription]); } + } else if ([fieldValue isKindOfClass:[FSTServerTimestampFieldValue class]]) { [context appendToFieldTransformsWithFieldPath:*context.path transformOperation:absl::make_unique<ServerTimestampTransform>( ServerTimestampTransform::Get())]; + + } else if ([fieldValue isKindOfClass:[FSTArrayUnionFieldValue class]]) { + std::vector<FSTFieldValue *> parsedElements = + [self parseArrayTransformElements:((FSTArrayUnionFieldValue *)fieldValue).elements]; + auto array_union = + absl::make_unique<ArrayTransform>(TransformOperation::Type::ArrayUnion, parsedElements); + [context appendToFieldTransformsWithFieldPath:*context.path + transformOperation:std::move(array_union)]; + + } else if ([fieldValue isKindOfClass:[FSTArrayRemoveFieldValue class]]) { + std::vector<FSTFieldValue *> parsedElements = + [self parseArrayTransformElements:((FSTArrayRemoveFieldValue *)fieldValue).elements]; + auto array_remove = + absl::make_unique<ArrayTransform>(TransformOperation::Type::ArrayRemove, parsedElements); + [context appendToFieldTransformsWithFieldPath:*context.path + transformOperation:std::move(array_remove)]; + } else { FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([fieldValue class])); } @@ -753,6 +776,22 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { } } +- (std::vector<FSTFieldValue *>)parseArrayTransformElements:(NSArray<id> *)elements { + std::vector<FSTFieldValue *> results; + for (id element in elements) { + // Although array transforms are used with writes, the actual elements being unioned or removed + // are not considered writes since they cannot contain any FieldValue sentinels, etc. + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceArgument + path:absl::make_unique<FieldPath>(FieldPath::EmptyPath())]; + FSTFieldValue *parsedElement = [self parseData:element context:context]; + FSTAssert(parsedElement && context.fieldTransforms->size() == 0, + @"Failed to properly parse array transform element: %@", element); + results.push_back(parsedElement); + } + return results; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFieldValue.h b/Firestore/Source/Public/FIRFieldValue.h index 11a0da0..d896587 100644 --- a/Firestore/Source/Public/FIRFieldValue.h +++ b/Firestore/Source/Public/FIRFieldValue.h @@ -38,6 +38,29 @@ NS_SWIFT_NAME(FieldValue) */ + (instancetype)fieldValueForServerTimestamp NS_SWIFT_NAME(serverTimestamp()); +/** + * Returns a special value that can be used with setData() or updateData() that tells the server to + * union the given elements with any array value that already exists on the server. Each + * specified element that doesn't already exist in the array will be added to the end. If the + * field being modified is not already an array it will be overwritten with an array containing + * exactly the specified elements. + * + * @param elements The elements to union into the array. + * @return The FieldValue sentinel for use in a call to setData() or updateData(). + */ ++ (instancetype)fieldValueForArrayUnion:(NSArray<id> *)elements NS_SWIFT_NAME(arrayUnion(_:)); + +/** + * Returns a special value that can be used with setData() or updateData() that tells the server to + * remove the given elements from any array value that already exists on the server. All + * instances of each element specified will be removed from the array. If the field being + * modified is not already an array it will be overwritten with an empty array. + * + * @param elements The elements to remove from the array. + * @return The FieldValue sentinel for use in a call to setData() or updateData(). + */ ++ (instancetype)fieldValueForArrayRemove:(NSArray<id> *)elements NS_SWIFT_NAME(arrayRemove(_:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/model/transform_operations.h b/Firestore/core/src/firebase/firestore/model/transform_operations.h index 6ff4dbc..aad5a9b 100644 --- a/Firestore/core/src/firebase/firestore/model/transform_operations.h +++ b/Firestore/core/src/firebase/firestore/model/transform_operations.h @@ -17,6 +17,12 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_TRANSFORM_OPERATIONS_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_TRANSFORM_OPERATIONS_H_ +#include <vector> + +#if defined(__OBJC__) +#import "Firestore/Source/Model/FSTFieldValue.h" +#endif + namespace firebase { namespace firestore { namespace model { @@ -31,6 +37,8 @@ class TransformOperation { /** All the different kinds to TransformOperation. */ enum class Type { ServerTimestamp, + ArrayUnion, + ArrayRemove, Test, // Purely for test purpose. }; @@ -90,6 +98,65 @@ class ServerTimestampTransform : public TransformOperation { } }; +// TODO(mikelehen): ArrayTransform can only be used from Obj-C until we switch +// to using FieldValue instead of FSTFieldValue. +#if defined(__OBJC__) +/** + * Transforms an array via a union or remove operation (for convenience, we use + * this class for both Type::ArrayUnion and Type::ArrayRemove). + */ +class ArrayTransform : public TransformOperation { + public: + ArrayTransform(const Type& type, const std::vector<FSTFieldValue*> elements) + : type_(type), elements_(elements) { + } + + ~ArrayTransform() override { + } + + Type type() const override { + return type_; + } + + const std::vector<FSTFieldValue*>& elements() const { + return elements_; + } + + bool operator==(const TransformOperation& other) const override { + if (other.type() != type()) { + return false; + } + auto array_transform = static_cast<const ArrayTransform&>(other); + if (array_transform.elements_.size() != elements_.size()) { + return false; + } + for (int i = 0; i < elements_.size(); i++) { + if (![array_transform.elements_[i] isEqual:elements_[i]]) { + return false; + } + } + return true; + } + +#if defined(__OBJC__) + // For Objective-C++ hash; to be removed after migration. + // Do NOT use in C++ code. + NSUInteger Hash() const override { + NSUInteger result = 37; + result = 31 * result + (type() == Type::ArrayUnion ? 1231 : 1237); + for (FSTFieldValue* element : elements_) { + result = 31 * result + [element hash]; + } + return result; + } +#endif // defined(__OBJC__) + + private: + Type type_; + std::vector<FSTFieldValue*> elements_; +}; +#endif + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc b/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc index 0ef95db..ec0882a 100644 --- a/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc +++ b/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc @@ -46,6 +46,9 @@ TEST(TransformOperations, ServerTimestamp) { EXPECT_NE(transform, dummy); } +// TODO(mikelehen): Add ArrayTransform test once it no longer depends on +// FSTFieldValue and can be exposed to C++ code. + } // namespace model } // namespace firestore } // namespace firebase |