From 974353893e6a6a586b6901c6c74686593537a4f7 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 30 Jun 2020 22:27:41 -0700 Subject: [PATCH 1/5] Remove Mutation overhead from Lite SDK --- .../firestore/lite/test/dependencies.json | 244 +--- packages/firestore/src/api/field_value.ts | 2 +- .../src/local/local_documents_view.ts | 5 +- packages/firestore/src/local/local_store.ts | 10 +- packages/firestore/src/model/mutation.ts | 1010 +++++++++-------- .../firestore/src/model/mutation_batch.ts | 28 +- .../src/model/transform_operation.ts | 322 +++--- packages/firestore/src/remote/serializer.ts | 2 +- .../test/unit/model/mutation.test.ts | 95 +- .../test/unit/remote/serializer.helper.ts | 5 +- 10 files changed, 820 insertions(+), 903 deletions(-) diff --git a/packages/firestore/lite/test/dependencies.json b/packages/firestore/lite/test/dependencies.json index df88ad5b687..41462a2b7c1 100644 --- a/packages/firestore/lite/test/dependencies.json +++ b/packages/firestore/lite/test/dependencies.json @@ -199,7 +199,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -219,7 +218,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -236,19 +234,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99162 + "sizeInBytes": 91030 }, "DocumentReference": { "dependencies": { @@ -698,7 +694,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -718,7 +713,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -734,18 +728,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 85476 + "sizeInBytes": 77344 }, "QueryDocumentSnapshot": { "dependencies": { @@ -1053,19 +1045,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "Transaction$1", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 69588 + "sizeInBytes": 62673 }, "WriteBatch": { "dependencies": { @@ -1077,7 +1067,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -1095,18 +1084,12 @@ "fromDotSeparatedString", "fullyQualifiedPrefixPath", "geoPointEquals", - "getEncodedDatabaseId", "getLocalWriteTime", "hardAssert", "invalidClassError", - "invokeCommitRpc", - "isArray", - "isDouble", "isEmpty", - "isInteger", "isMapValue", "isNegativeZero", - "isNumber", "isPlainObject", "isSafeInteger", "isServerTimestamp", @@ -1136,23 +1119,15 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", "toBytes", - "toDocumentMask", "toDouble", - "toFieldTransform", "toInteger", - "toMutation", - "toMutationDocument", - "toName", "toNumber", - "toPrecondition", "toResourceName", "toTimestamp", - "toVersion", "tryGetCustomObjectType", "typeOrder", "uint8ArrayFromBinaryString", @@ -1166,8 +1141,6 @@ "valueEquals" ], "classes": [ - "ArrayRemoveTransformOperation", - "ArrayUnionTransformOperation", "BaseFieldPath", "BasePath", "Blob", @@ -1179,7 +1152,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -1192,10 +1164,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", - "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", "ObjectValueBuilder", @@ -1206,21 +1175,17 @@ "Precondition", "ResourcePath", "SerializableFieldValue", - "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", - "VerifyMutation", "WriteBatch" ], "variables": [] }, - "sizeInBytes": 72512 + "sizeInBytes": 56525 }, "addDoc": { "dependencies": { @@ -1247,7 +1212,6 @@ "canonifyTimestamp", "canonifyValue", "cast", - "coercedFieldValuesArray", "compareArrays", "compareBlobs", "compareDocs", @@ -1329,7 +1293,6 @@ "randomBytes", "refValue", "registerFirestore", - "serverTimestamp", "sortsBeforeDocument", "stringifyFilter", "stringifyOrderBy", @@ -1385,7 +1348,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -1405,9 +1367,7 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -1425,12 +1385,11 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "UserDataWriter", @@ -1438,19 +1397,16 @@ ], "variables": [] }, - "sizeInBytes": 109513 + "sizeInBytes": 97301 }, "arrayRemove": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "arrayRemove", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "createSentinelChildContext", @@ -1463,16 +1419,12 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "invalidClassError", - "isArray", "isEmpty", "isNegativeZero", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -1484,12 +1436,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -1500,7 +1446,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -1508,15 +1453,13 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateAtLeastNumberOfArgs", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "ArrayRemoveFieldValueImpl", @@ -1545,23 +1488,21 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42383 + "sizeInBytes": 36971 }, "arrayUnion": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "arrayUnion", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "createSentinelChildContext", @@ -1574,16 +1515,12 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "invalidClassError", - "isArray", "isEmpty", "isNegativeZero", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -1595,12 +1532,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -1611,7 +1542,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -1619,15 +1549,13 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateAtLeastNumberOfArgs", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "ArrayUnionFieldValueImpl", @@ -1656,11 +1584,12 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42391 + "sizeInBytes": 36963 }, "collection": { "dependencies": { @@ -1804,7 +1733,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -1824,7 +1752,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -1841,19 +1768,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99790 + "sizeInBytes": 91658 }, "collectionGroup": { "dependencies": { @@ -1995,7 +1920,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -2015,7 +1939,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -2032,50 +1955,36 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99222 + "sizeInBytes": 91090 }, "deleteDoc": { "dependencies": { "functions": [ "argToString", - "arrayEquals", - "binaryStringFromUint8Array", - "blobEquals", "cast", - "coercedFieldValuesArray", "createMetadata", "debugAssert", "debugCast", - "decodeBase64", "deleteDoc", - "encodeBase64", "fail", "formatJSON", "fullyQualifiedPrefixPath", - "geoPointEquals", "getEncodedDatabaseId", - "getLocalWriteTime", "hardAssert", "invokeCommitRpc", - "isArray", "isDouble", "isInteger", - "isMapValue", - "isNegativeZero", "isNumber", - "isServerTimestamp", "loadProtos", "logDebug", "logError", @@ -2085,76 +1994,54 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", - "timestampEquals", "toDocumentMask", - "toDouble", "toFieldTransform", - "toInteger", "toMutation", "toMutationDocument", "toName", "toPrecondition", "toResourceName", "toTimestamp", - "toVersion", - "typeOrder", - "uint8ArrayFromBinaryString", - "valueEquals" + "toVersion" ], "classes": [ "ArrayRemoveTransformOperation", "ArrayUnionTransformOperation", "BasePath", - "ByteString", "DatabaseId", "DatabaseInfo", "Datastore", "DatastoreImpl", "Deferred", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", - "FieldPath", "FirebaseCredentialsProvider", "Firestore", "FirestoreError", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "PatchMutation", "Precondition", "ResourcePath", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", - "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 50238 + "sizeInBytes": 26210 }, "deleteField": { "dependencies": { @@ -2349,7 +2236,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -2369,7 +2255,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -2386,19 +2271,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 100611 + "sizeInBytes": 92479 }, "documentId": { "dependencies": { @@ -2734,6 +2617,7 @@ "uint8ArrayFromBinaryString", "validateArgType", "validateExactNumberOfArgs", + "validateHasExplicitOrderByForLimitToLast", "validateNamedArrayAtLeastNumberOfElements", "validatePlainObject", "validatePositiveNumber", @@ -2798,23 +2682,20 @@ "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91235 + "sizeInBytes": 84898 }, "increment": { "dependencies": { "functions": [ "argToString", - "arrayEquals", "assertUint8ArrayAvailable", "binaryStringFromUint8Array", - "blobEquals", "cast", "createError", "createMetadata", @@ -2828,8 +2709,6 @@ "formatJSON", "formatPlural", "fullyQualifiedPrefixPath", - "geoPointEquals", - "getLocalWriteTime", "hardAssert", "increment", "invalidClassError", @@ -2840,7 +2719,6 @@ "isNumber", "isPlainObject", "isSafeInteger", - "isServerTimestamp", "isWrite", "loadProtos", "logDebug", @@ -2852,12 +2730,6 @@ "newDatastore", "newSerializer", "nodePromise", - "normalizeByteString", - "normalizeNumber", - "normalizeTimestamp", - "numberEquals", - "objectEquals", - "objectSize", "ordinal", "parseArray", "parseData", @@ -2868,7 +2740,6 @@ "registerFirestore", "terminate", "terminateDatastore", - "timestampEquals", "toBytes", "toDouble", "toInteger", @@ -2876,14 +2747,12 @@ "toResourceName", "toTimestamp", "tryGetCustomObjectType", - "typeOrder", "uint8ArrayFromBinaryString", "validateArgType", "validateExactNumberOfArgs", "validatePlainObject", "validateType", - "valueDescription", - "valueEquals" + "valueDescription" ], "classes": [ "BasePath", @@ -2912,11 +2781,12 @@ "SerializableFieldValue", "StreamBridge", "Timestamp", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 42341 + "sizeInBytes": 36914 }, "initializeFirestore": { "dependencies": { @@ -3104,7 +2974,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3124,7 +2993,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3141,19 +3009,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99505 + "sizeInBytes": 91373 }, "queryEqual": { "dependencies": { @@ -3270,7 +3136,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3290,7 +3155,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3306,18 +3170,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 85680 + "sizeInBytes": 77548 }, "refEqual": { "dependencies": { @@ -3460,7 +3322,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -3480,7 +3341,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -3497,19 +3357,17 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "TargetImpl", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 99447 + "sizeInBytes": 91315 }, "runTransaction": { "dependencies": { @@ -3522,7 +3380,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -3558,7 +3415,6 @@ "invalidClassError", "invokeBatchGetDocumentsRpc", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isIndexedDbTransactionError", @@ -3599,7 +3455,6 @@ "primitiveComparator", "registerFirestore", "runTransaction", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -3684,7 +3539,7 @@ "Transaction$1", "TransactionRunner", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "UserDataWriter", @@ -3692,7 +3547,7 @@ ], "variables": [] }, - "sizeInBytes": 91963 + "sizeInBytes": 81867 }, "serverTimestamp": { "dependencies": { @@ -3717,7 +3572,6 @@ "primitiveComparator", "registerFirestore", "serverTimestamp", - "serverTimestamp$1", "terminate", "terminateDatastore" ], @@ -3740,11 +3594,12 @@ "ServerTimestampFieldValueImpl", "ServerTimestampTransform", "StreamBridge", + "TransformOperation", "User" ], "variables": [] }, - "sizeInBytes": 19024 + "sizeInBytes": 18118 }, "setDoc": { "dependencies": { @@ -3756,7 +3611,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -3779,7 +3633,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -3815,7 +3668,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "setDoc", "terminate", "terminateDatastore", @@ -3858,7 +3710,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -3870,9 +3721,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -3886,18 +3735,17 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 70801 + "sizeInBytes": 58711 }, "setLogLevel": { "dependencies": { @@ -4060,7 +3908,6 @@ "DatastoreImpl", "Deferred", "DeleteFieldValueImpl", - "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", @@ -4080,7 +3927,6 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "MaybeDocument", "Mutation", "OAuthToken", "ObjectValue", @@ -4097,18 +3943,16 @@ "ResourcePath", "SerializableFieldValue", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 86413 + "sizeInBytes": 78281 }, "terminate": { "dependencies": { @@ -4163,7 +4007,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -4186,7 +4029,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -4222,7 +4064,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -4265,7 +4106,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -4278,9 +4118,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -4294,18 +4132,17 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation" ], "variables": [] }, - "sizeInBytes": 70838 + "sizeInBytes": 58748 }, "writeBatch": { "dependencies": { @@ -4317,7 +4154,6 @@ "binaryStringFromUint8Array", "blobEquals", "cast", - "coercedFieldValuesArray", "createError", "createMetadata", "debugAssert", @@ -4340,7 +4176,6 @@ "hardAssert", "invalidClassError", "invokeCommitRpc", - "isArray", "isDouble", "isEmpty", "isInteger", @@ -4376,7 +4211,6 @@ "parseSentinelFieldValue", "primitiveComparator", "registerFirestore", - "serverTimestamp", "terminate", "terminateDatastore", "timestampEquals", @@ -4420,7 +4254,6 @@ "Deferred", "DeleteFieldValueImpl", "DeleteMutation", - "Document", "DocumentKeyReference", "DocumentReference", "FieldMask", @@ -4433,9 +4266,7 @@ "GeoPoint", "GrpcConnection", "JsonProtoSerializer", - "MaybeDocument", "Mutation", - "NoDocument", "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", @@ -4449,11 +4280,10 @@ "SerializableFieldValue", "ServerTimestampTransform", "SetMutation", - "SnapshotVersion", "StreamBridge", "Timestamp", "TransformMutation", - "UnknownDocument", + "TransformOperation", "User", "UserDataReader", "VerifyMutation", @@ -4461,6 +4291,6 @@ ], "variables": [] }, - "sizeInBytes": 72592 + "sizeInBytes": 60620 } } \ No newline at end of file diff --git a/packages/firestore/src/api/field_value.ts b/packages/firestore/src/api/field_value.ts index d1dd98db81a..731445539b0 100644 --- a/packages/firestore/src/api/field_value.ts +++ b/packages/firestore/src/api/field_value.ts @@ -123,7 +123,7 @@ export class ServerTimestampFieldValueImpl extends SerializableFieldValue { } _toFieldTransform(context: ParseContext): FieldTransform { - return new FieldTransform(context.path!, ServerTimestampTransform.instance); + return new FieldTransform(context.path!, new ServerTimestampTransform()); } isEqual(other: FieldValue): boolean { diff --git a/packages/firestore/src/local/local_documents_view.ts b/packages/firestore/src/local/local_documents_view.ts index 75c43f8dafc..16694311d9e 100644 --- a/packages/firestore/src/local/local_documents_view.ts +++ b/packages/firestore/src/local/local_documents_view.ts @@ -35,7 +35,7 @@ import { ResourcePath } from '../model/path'; import { debugAssert } from '../util/assert'; import { IndexManager } from './index_manager'; import { MutationQueue } from './mutation_queue'; -import { PatchMutation } from '../model/mutation'; +import { applyMutationToLocalView, PatchMutation } from '../model/mutation'; import { PersistenceTransaction } from './persistence'; import { PersistencePromise } from './persistence_promise'; import { RemoteDocumentCache } from './remote_document_cache'; @@ -258,7 +258,8 @@ export class LocalDocumentsView { for (const mutation of batch.mutations) { const key = mutation.key; const baseDoc = results.get(key); - const mutatedDoc = mutation.applyToLocalView( + const mutatedDoc = applyMutationToLocalView( + mutation, baseDoc, baseDoc, batch.localWriteTime diff --git a/packages/firestore/src/local/local_store.ts b/packages/firestore/src/local/local_store.ts index c996fe532c1..151afa3335b 100644 --- a/packages/firestore/src/local/local_store.ts +++ b/packages/firestore/src/local/local_store.ts @@ -30,7 +30,12 @@ import { } from '../model/collections'; import { MaybeDocument, NoDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; -import { Mutation, PatchMutation, Precondition } from '../model/mutation'; +import { + Mutation, + PatchMutation, + Precondition, + extractMutationBaseValue +} from '../model/mutation'; import { BATCHID_UNKNOWN, MutationBatch, @@ -449,7 +454,8 @@ class LocalStoreImpl implements LocalStore { const baseMutations: Mutation[] = []; for (const mutation of mutations) { - const baseValue = mutation.extractBaseValue( + const baseValue = extractMutationBaseValue( + mutation, existingDocs.get(mutation.key) ); if (baseValue != null) { diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index 9777ba25229..bef77cfd819 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -19,7 +19,7 @@ import * as api from '../protos/firestore_proto_api'; import { Timestamp } from '../api/timestamp'; import { SnapshotVersion } from '../core/snapshot_version'; -import { debugAssert, fail, hardAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { Document, @@ -30,7 +30,13 @@ import { import { DocumentKey } from './document_key'; import { ObjectValue, ObjectValueBuilder } from './object_value'; import { FieldPath } from './path'; -import { TransformOperation } from './transform_operation'; +import { + applyTransformOperationToLocalView, + applyTransformOperationToRemoteDocument, + computeTransformOperationBaseValue, + TransformOperation, + transformOperationEquals +} from './transform_operation'; import { arrayEquals } from '../util/misc'; /** @@ -81,12 +87,16 @@ export class FieldTransform { readonly field: FieldPath, readonly transform: TransformOperation ) {} +} - isEqual(other: FieldTransform): boolean { - return ( - this.field.isEqual(other.field) && this.transform.isEqual(other.transform) - ); - } +export function fieldTransformEquals( + l: FieldTransform, + r: FieldTransform +): boolean { + return ( + l.field.isEqual(r.field) && + transformOperationEquals(l.transform, r.transform) + ); } /** The result of successfully applying a mutation to the backend. */ @@ -158,24 +168,6 @@ export class Precondition { return this.updateTime === undefined && this.exists === undefined; } - /** - * Returns true if the preconditions is valid for the given document - * (or null if no document is available). - */ - isValidFor(maybeDoc: MaybeDocument | null): boolean { - if (this.updateTime !== undefined) { - return ( - maybeDoc instanceof Document && - maybeDoc.version.isEqual(this.updateTime) - ); - } else if (this.exists !== undefined) { - return this.exists === maybeDoc instanceof Document; - } else { - debugAssert(this.isNone, 'Precondition should be empty'); - return true; - } - } - isEqual(other: Precondition): boolean { return ( this.exists === other.exists && @@ -186,6 +178,27 @@ export class Precondition { } } +/** + * Returns true if the preconditions is valid for the given document + * (or null if no document is available). + */ +export function preconditionIsValidForDocument( + precondition: Precondition, + maybeDoc: MaybeDocument | null +): boolean { + if (precondition.updateTime !== undefined) { + return ( + maybeDoc instanceof Document && + maybeDoc.version.isEqual(precondition.updateTime) + ); + } else if (precondition.exists !== undefined) { + return precondition.exists === maybeDoc instanceof Document; + } else { + debugAssert(precondition.isNone, 'Precondition should be empty'); + return true; + } +} + /** * A mutation describes a self-contained change to a document. Mutations can * create, replace, delete, and update subsets of documents. @@ -239,89 +252,187 @@ export abstract class Mutation { abstract readonly type: MutationType; abstract readonly key: DocumentKey; abstract readonly precondition: Precondition; +} - /** - * Applies this mutation to the given MaybeDocument or null for the purposes - * of computing a new remote document. If the input document doesn't match the - * expected state (e.g. it is null or outdated), an `UnknownDocument` can be - * returned. - * - * @param maybeDoc The document to mutate. The input document can be null if - * the client has no knowledge of the pre-mutation state of the document. - * @param mutationResult The result of applying the mutation from the backend. - * @return The mutated document. The returned document may be an - * UnknownDocument if the mutation could not be applied to the locally - * cached base document. - */ - abstract applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument; +/** + * Applies this mutation to the given MaybeDocument or null for the purposes + * of computing a new remote document. If the input document doesn't match the + * expected state (e.g. it is null or outdated), an `UnknownDocument` can be + * returned. + * + * @param mutation The mutation to apply. + * @param maybeDoc The document to mutate. The input document can be null if + * the client has no knowledge of the pre-mutation state of the document. + * @param mutationResult The result of applying the mutation from the backend. + * @return The mutated document. The returned document may be an + * UnknownDocument if the mutation could not be applied to the locally + * cached base document. + */ +export function applyMutationToRemoteDocument( + mutation: Mutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): MaybeDocument { + verifyMutationKeyMatches(mutation, maybeDoc); + if (mutation instanceof SetMutation) { + return applySetMutationToRemoteDocument(mutation, maybeDoc, mutationResult); + } else if (mutation instanceof PatchMutation) { + return applyPatchMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } else if (mutation instanceof TransformMutation) { + return applyTransformMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } else { + debugAssert( + mutation instanceof DeleteMutation, + 'Unexpected mutation type: ' + mutation + ); + return applyDeleteMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); + } +} - /** - * Applies this mutation to the given MaybeDocument or null for the purposes - * of computing the new local view of a document. Both the input and returned - * documents can be null. - * - * @param maybeDoc The document to mutate. The input document can be null if - * the client has no knowledge of the pre-mutation state of the document. - * @param baseDoc The state of the document prior to this mutation batch. The - * input document can be null if the client has no knowledge of the - * pre-mutation state of the document. - * @param localWriteTime A timestamp indicating the local write time of the - * batch this mutation is a part of. - * @return The mutated document. The returned document may be null, but only - * if maybeDoc was null and the mutation would not create a new document. - */ - abstract applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null; +/** + * Applies this mutation to the given MaybeDocument or null for the purposes + * of computing the new local view of a document. Both the input and returned + * documents can be null. + * + * @param mutation The mutation to apply. + * @param maybeDoc The document to mutate. The input document can be null if + * the client has no knowledge of the pre-mutation state of the document. + * @param baseDoc The state of the document prior to this mutation batch. The + * input document can be null if the client has no knowledge of the + * pre-mutation state of the document. + * @param localWriteTime A timestamp indicating the local write time of the + * batch this mutation is a part of. + * @return The mutated document. The returned document may be null, but only + * if maybeDoc was null and the mutation would not create a new document. + */ +export function applyMutationToLocalView( + mutation: Mutation, + maybeDoc: MaybeDocument | null, + baseDoc: MaybeDocument | null, + localWriteTime: Timestamp +): MaybeDocument | null { + verifyMutationKeyMatches(mutation, maybeDoc); + + if (mutation instanceof SetMutation) { + return applySetMutationToLocalView(mutation, maybeDoc); + } else if (mutation instanceof PatchMutation) { + return applyPatchMutationToLocalView(mutation, maybeDoc); + } else if (mutation instanceof TransformMutation) { + return applyTransformMutationToLocalView( + mutation, + maybeDoc, + localWriteTime, + baseDoc + ); + } else { + debugAssert( + mutation instanceof DeleteMutation, + 'Unexpected mutation type: ' + mutation + ); + return applyDeleteMutationToLocalView(mutation, maybeDoc); + } +} - /** - * If this mutation is not idempotent, returns the base value to persist with - * this mutation. If a base value is returned, the mutation is always applied - * to this base value, even if document has already been updated. - * - * The base value is a sparse object that consists of only the document - * fields for which this mutation contains a non-idempotent transformation - * (e.g. a numeric increment). The provided value guarantees consistent - * behavior for non-idempotent transforms and allow us to return the same - * latency-compensated value even if the backend has already applied the - * mutation. The base value is null for idempotent mutations, as they can be - * re-played even if the backend has already applied them. - * - * @return a base value to store along with the mutation, or null for - * idempotent mutations. - */ - abstract extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null; +/** + * If this mutation is not idempotent, returns the base value to persist with + * this mutation. If a base value is returned, the mutation is always applied + * to this base value, even if document has already been updated. + * + * The base value is a sparse object that consists of only the document + * fields for which this mutation contains a non-idempotent transformation + * (e.g. a numeric increment). The provided value guarantees consistent + * behavior for non-idempotent transforms and allow us to return the same + * latency-compensated value even if the backend has already applied the + * mutation. The base value is null for idempotent mutations, as they can be + * re-played even if the backend has already applied them. + * + * @return a base value to store along with the mutation, or null for + * idempotent mutations. + */ +export function extractMutationBaseValue( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): ObjectValue | null { + if (mutation instanceof TransformMutation) { + return extractTransformMutationBaseValue(mutation, maybeDoc); + } + return null; +} - abstract isEqual(other: Mutation): boolean; +export function mutationEquals(left: Mutation, right: Mutation): boolean { + if (left.type !== right.type) { + return false; + } - protected verifyKeyMatches(maybeDoc: MaybeDocument | null): void { - if (maybeDoc != null) { - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only apply a mutation to a document with the same key' - ); - } + if (!left.key.isEqual(right.key)) { + return false; } - /** - * Returns the version from the given document for use as the result of a - * mutation. Mutations are defined to return the version of the base document - * only if it is an existing document. Deleted and unknown documents have a - * post-mutation version of SnapshotVersion.min(). - */ - protected static getPostMutationVersion( - maybeDoc: MaybeDocument | null - ): SnapshotVersion { - if (maybeDoc instanceof Document) { - return maybeDoc.version; - } else { - return SnapshotVersion.min(); - } + if (!left.precondition.isEqual(right.precondition)) { + return false; + } + + if (left.type === MutationType.Set) { + return (left as SetMutation).value.isEqual((right as SetMutation).value); + } + + if (left.type === MutationType.Patch) { + return ( + (left as PatchMutation).data.isEqual((right as PatchMutation).data) && + (left as PatchMutation).fieldMask.isEqual( + (right as PatchMutation).fieldMask + ) + ); + } + + if (left.type === MutationType.Transform) { + return arrayEquals( + (left as TransformMutation).fieldTransforms, + (left as TransformMutation).fieldTransforms, + (l, r) => fieldTransformEquals(l, r) + ); + } + + return true; +} + +function verifyMutationKeyMatches( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): void { + if (maybeDoc != null) { + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only apply a mutation to a document with the same key' + ); + } +} + +/** + * Returns the version from the given document for use as the result of a + * mutation. Mutations are defined to return the version of the base document + * only if it is an existing document. Deleted and unknown documents have a + * post-mutation version of SnapshotVersion.min(). + */ +function getPostMutationVersion( + maybeDoc: MaybeDocument | null +): SnapshotVersion { + if (maybeDoc instanceof Document) { + return maybeDoc.version; + } else { + return SnapshotVersion.min(); } } @@ -339,57 +450,38 @@ export class SetMutation extends Mutation { } readonly type: MutationType = MutationType.Set; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by SetMutation.' - ); - - // Unlike applyToLocalView, if we're applying a mutation to a remote - // document the server has accepted the mutation so the precondition must - // have held. - - const version = mutationResult.version; - return new Document(this.key, version, this.value, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } - - const version = Mutation.getPostMutationVersion(maybeDoc); - return new Document(this.key, version, this.value, { - hasLocalMutations: true - }); - } +function applySetMutationToRemoteDocument( + mutation: SetMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): Document { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by SetMutation.' + ); + + // Unlike applySetMutationToLocalView, if we're applying a mutation to a + // remote document the server has accepted the mutation so the precondition + // must have held. + return new Document(mutation.key, mutationResult.version, mutation.value, { + hasCommittedMutations: true + }); +} - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; +function applySetMutationToLocalView( + mutation: SetMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - isEqual(other: Mutation): boolean { - return ( - other instanceof SetMutation && - this.key.isEqual(other.key) && - this.value.isEqual(other.value) && - this.precondition.isEqual(other.precondition) - ); - } + const version = getPostMutationVersion(maybeDoc); + return new Document(mutation.key, version, mutation.value, { + hasLocalMutations: true + }); } /** @@ -416,92 +508,78 @@ export class PatchMutation extends Mutation { } readonly type: MutationType = MutationType.Patch; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by PatchMutation.' - ); - - if (!this.precondition.isValidFor(maybeDoc)) { - // Since the mutation was not rejected, we know that the precondition - // matched on the backend. We therefore must not have the expected version - // of the document in our cache and return an UnknownDocument with the - // known updateTime. - return new UnknownDocument(this.key, mutationResult.version); - } - - const newData = this.patchDocument(maybeDoc); - return new Document(this.key, mutationResult.version, newData, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } +function applyPatchMutationToRemoteDocument( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): MaybeDocument { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by PatchMutation.' + ); + + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + // Since the mutation was not rejected, we know that the precondition + // matched on the backend. We therefore must not have the expected version + // of the document in our cache and return an UnknownDocument with the + // known updateTime. + return new UnknownDocument(mutation.key, mutationResult.version); + } + + const newData = patchDocument(mutation, maybeDoc); + return new Document(mutation.key, mutationResult.version, newData, { + hasCommittedMutations: true + }); +} - const version = Mutation.getPostMutationVersion(maybeDoc); - const newData = this.patchDocument(maybeDoc); - return new Document(this.key, version, newData, { - hasLocalMutations: true - }); +function applyPatchMutationToLocalView( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; - } + const version = getPostMutationVersion(maybeDoc); + const newData = patchDocument(mutation, maybeDoc); + return new Document(mutation.key, version, newData, { + hasLocalMutations: true + }); +} - isEqual(other: Mutation): boolean { - return ( - other instanceof PatchMutation && - this.key.isEqual(other.key) && - this.fieldMask.isEqual(other.fieldMask) && - this.precondition.isEqual(other.precondition) - ); - } +/** + * Patches the data of document if available or creates a new document. Note + * that this does not check whether or not the precondition of this patch + * holds. + */ +function patchDocument( + mutation: PatchMutation, + maybeDoc: MaybeDocument | null +): ObjectValue { + let data: ObjectValue; + if (maybeDoc instanceof Document) { + data = maybeDoc.data(); + } else { + data = ObjectValue.empty(); + } + return patchObject(mutation, data); +} - /** - * Patches the data of document if available or creates a new document. Note - * that this does not check whether or not the precondition of this patch - * holds. - */ - private patchDocument(maybeDoc: MaybeDocument | null): ObjectValue { - let data: ObjectValue; - if (maybeDoc instanceof Document) { - data = maybeDoc.data(); - } else { - data = ObjectValue.empty(); - } - return this.patchObject(data); - } - - private patchObject(data: ObjectValue): ObjectValue { - const builder = new ObjectValueBuilder(data); - this.fieldMask.fields.forEach(fieldPath => { - if (!fieldPath.isEmpty()) { - const newValue = this.data.field(fieldPath); - if (newValue !== null) { - builder.set(fieldPath, newValue); - } else { - builder.delete(fieldPath); - } +function patchObject(mutation: PatchMutation, data: ObjectValue): ObjectValue { + const builder = new ObjectValueBuilder(data); + mutation.fieldMask.fields.forEach(fieldPath => { + if (!fieldPath.isEmpty()) { + const newValue = mutation.data.field(fieldPath); + if (newValue !== null) { + builder.set(fieldPath, newValue); + } else { + builder.delete(fieldPath); } - }); - return builder.build(); - } + } + }); + return builder.build(); } /** @@ -527,211 +605,216 @@ export class TransformMutation extends Mutation { ) { super(); } +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - hardAssert( - mutationResult.transformResults != null, - 'Transform results missing for TransformMutation.' - ); - - if (!this.precondition.isValidFor(maybeDoc)) { - // Since the mutation was not rejected, we know that the precondition - // matched on the backend. We therefore must not have the expected version - // of the document in our cache and return an UnknownDocument with the - // known updateTime. - return new UnknownDocument(this.key, mutationResult.version); - } - - const doc = this.requireDocument(maybeDoc); - const transformResults = this.serverTransformResults( - maybeDoc, - mutationResult.transformResults! - ); +function applyTransformMutationToRemoteDocument( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): Document | UnknownDocument { + hardAssert( + mutationResult.transformResults != null, + 'Transform results missing for TransformMutation.' + ); + + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + // Since the mutation was not rejected, we know that the precondition + // matched on the backend. We therefore must not have the expected version + // of the document in our cache and return an UnknownDocument with the + // known updateTime. + return new UnknownDocument(mutation.key, mutationResult.version); + } + + const doc = requireDocument(mutation, maybeDoc); + const transformResults = serverTransformResults( + mutation.fieldTransforms, + maybeDoc, + mutationResult.transformResults! + ); + + const version = mutationResult.version; + const newData = transformObject(mutation, doc.data(), transformResults); + return new Document(mutation.key, version, newData, { + hasCommittedMutations: true + }); +} - const version = mutationResult.version; - const newData = this.transformObject(doc.data(), transformResults); - return new Document(this.key, version, newData, { - hasCommittedMutations: true - }); +function applyTransformMutationToLocalView( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null, + localWriteTime: Timestamp, + baseDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } + const doc = requireDocument(mutation, maybeDoc); + const transformResults = localTransformResults( + mutation.fieldTransforms, + localWriteTime, + maybeDoc, + baseDoc + ); + const newData = transformObject(mutation, doc.data(), transformResults); + return new Document(mutation.key, doc.version, newData, { + hasLocalMutations: true + }); +} - const doc = this.requireDocument(maybeDoc); - const transformResults = this.localTransformResults( - localWriteTime, - maybeDoc, - baseDoc +function extractTransformMutationBaseValue( + mutation: TransformMutation, + maybeDoc: MaybeDocument | null | Document +): ObjectValue | null { + let baseObject: ObjectValueBuilder | null = null; + for (const fieldTransform of mutation.fieldTransforms) { + const existingValue = + maybeDoc instanceof Document + ? maybeDoc.field(fieldTransform.field) + : undefined; + const coercedValue = computeTransformOperationBaseValue( + fieldTransform.transform, + existingValue || null ); - const newData = this.transformObject(doc.data(), transformResults); - return new Document(this.key, doc.version, newData, { - hasLocalMutations: true - }); - } - - extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null { - let baseObject: ObjectValueBuilder | null = null; - for (const fieldTransform of this.fieldTransforms) { - const existingValue = - maybeDoc instanceof Document - ? maybeDoc.field(fieldTransform.field) - : undefined; - const coercedValue = fieldTransform.transform.computeBaseValue( - existingValue || null - ); - - if (coercedValue != null) { - if (baseObject == null) { - baseObject = new ObjectValueBuilder().set( - fieldTransform.field, - coercedValue - ); - } else { - baseObject = baseObject.set(fieldTransform.field, coercedValue); - } + + if (coercedValue != null) { + if (baseObject == null) { + baseObject = new ObjectValueBuilder().set( + fieldTransform.field, + coercedValue + ); + } else { + baseObject = baseObject.set(fieldTransform.field, coercedValue); } } - return baseObject ? baseObject.build() : null; } + return baseObject ? baseObject.build() : null; +} - isEqual(other: Mutation): boolean { - return ( - other instanceof TransformMutation && - this.key.isEqual(other.key) && - arrayEquals(this.fieldTransforms, other.fieldTransforms, (l, r) => - l.isEqual(r) - ) && - this.precondition.isEqual(other.precondition) - ); - } +/** + * Asserts that the given MaybeDocument is actually a Document and verifies + * that it matches the key for this mutation. Since we only support + * transformations with precondition exists this method is guaranteed to be + * safe. + */ +function requireDocument( + mutation: Mutation, + maybeDoc: MaybeDocument | null +): Document { + debugAssert( + maybeDoc instanceof Document, + 'Unknown MaybeDocument type ' + maybeDoc + ); + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only transform a document with the same key' + ); + return maybeDoc; +} - /** - * Asserts that the given MaybeDocument is actually a Document and verifies - * that it matches the key for this mutation. Since we only support - * transformations with precondition exists this method is guaranteed to be - * safe. - */ - private requireDocument(maybeDoc: MaybeDocument | null): Document { - debugAssert( - maybeDoc instanceof Document, - 'Unknown MaybeDocument type ' + maybeDoc - ); - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only transform a document with the same key' +/** + * Creates a list of "transform results" (a transform result is a field value + * representing the result of applying a transform) for use after a + * TransformMutation has been acknowledged by the server. + * + * @param fieldTransforms The field transforms to apply the result to. + * @param baseDoc The document prior to applying this mutation batch. + * @param serverTransformResults The transform results received by the server. + * @return The transform results list. + */ +function serverTransformResults( + fieldTransforms: FieldTransform[], + baseDoc: MaybeDocument | null, + serverTransformResults: Array +): api.Value[] { + const transformResults: api.Value[] = []; + hardAssert( + fieldTransforms.length === serverTransformResults.length, + `server transform result count (${serverTransformResults.length}) ` + + `should match field transform count (${fieldTransforms.length})` + ); + + for (let i = 0; i < serverTransformResults.length; i++) { + const fieldTransform = fieldTransforms[i]; + const transform = fieldTransform.transform; + let previousValue: api.Value | null = null; + if (baseDoc instanceof Document) { + previousValue = baseDoc.field(fieldTransform.field); + } + transformResults.push( + applyTransformOperationToRemoteDocument( + transform, + previousValue, + serverTransformResults[i] + ) ); - return maybeDoc; } + return transformResults; +} - /** - * Creates a list of "transform results" (a transform result is a field value - * representing the result of applying a transform) for use after a - * TransformMutation has been acknowledged by the server. - * - * @param baseDoc The document prior to applying this mutation batch. - * @param serverTransformResults The transform results received by the server. - * @return The transform results list. - */ - private serverTransformResults( - baseDoc: MaybeDocument | null, - serverTransformResults: Array - ): api.Value[] { - const transformResults: api.Value[] = []; - hardAssert( - this.fieldTransforms.length === serverTransformResults.length, - `server transform result count (${serverTransformResults.length}) ` + - `should match field transform count (${this.fieldTransforms.length})` - ); - - for (let i = 0; i < serverTransformResults.length; i++) { - const fieldTransform = this.fieldTransforms[i]; - const transform = fieldTransform.transform; - let previousValue: api.Value | null = null; - if (baseDoc instanceof Document) { - previousValue = baseDoc.field(fieldTransform.field); - } - transformResults.push( - transform.applyToRemoteDocument( - previousValue, - serverTransformResults[i] - ) - ); +/** + * Creates a list of "transform results" (a transform result is a field value + * representing the result of applying a transform) for use when applying a + * TransformMutation locally. + * + * @param fieldTransforms The field transforms to apply the result to. + * @param localWriteTime The local time of the transform mutation (used to + * generate ServerTimestampValues). + * @param maybeDoc The current state of the document after applying all + * previous mutations. + * @param baseDoc The document prior to applying this mutation batch. + * @return The transform results list. + */ +function localTransformResults( + fieldTransforms: FieldTransform[], + localWriteTime: Timestamp, + maybeDoc: MaybeDocument | null, + baseDoc: MaybeDocument | null +): api.Value[] { + const transformResults: api.Value[] = []; + for (const fieldTransform of fieldTransforms) { + const transform = fieldTransform.transform; + + let previousValue: api.Value | null = null; + if (maybeDoc instanceof Document) { + previousValue = maybeDoc.field(fieldTransform.field); } - return transformResults; - } - /** - * Creates a list of "transform results" (a transform result is a field value - * representing the result of applying a transform) for use when applying a - * TransformMutation locally. - * - * @param localWriteTime The local time of the transform mutation (used to - * generate ServerTimestampValues). - * @param maybeDoc The current state of the document after applying all - * previous mutations. - * @param baseDoc The document prior to applying this mutation batch. - * @return The transform results list. - */ - private localTransformResults( - localWriteTime: Timestamp, - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null - ): api.Value[] { - const transformResults: api.Value[] = []; - for (const fieldTransform of this.fieldTransforms) { - const transform = fieldTransform.transform; - - let previousValue: api.Value | null = null; - if (maybeDoc instanceof Document) { - previousValue = maybeDoc.field(fieldTransform.field); - } - - if (previousValue === null && baseDoc instanceof Document) { - // If the current document does not contain a value for the mutated - // field, use the value that existed before applying this mutation - // batch. This solves an edge case where a PatchMutation clears the - // values in a nested map before the TransformMutation is applied. - previousValue = baseDoc.field(fieldTransform.field); - } - - transformResults.push( - transform.applyToLocalView(previousValue, localWriteTime) - ); + if (previousValue === null && baseDoc instanceof Document) { + // If the current document does not contain a value for the mutated + // field, use the value that existed before applying this mutation + // batch. This solves an edge case where a PatchMutation clears the + // values in a nested map before the TransformMutation is applied. + previousValue = baseDoc.field(fieldTransform.field); } - return transformResults; - } - private transformObject( - data: ObjectValue, - transformResults: api.Value[] - ): ObjectValue { - debugAssert( - transformResults.length === this.fieldTransforms.length, - 'TransformResults length mismatch.' + transformResults.push( + applyTransformOperationToLocalView( + transform, + previousValue, + localWriteTime + ) ); - - const builder = new ObjectValueBuilder(data); - for (let i = 0; i < this.fieldTransforms.length; i++) { - const fieldTransform = this.fieldTransforms[i]; - const fieldPath = fieldTransform.field; - builder.set(fieldPath, transformResults[i]); - } - return builder.build(); } + return transformResults; +} + +function transformObject( + mutation: TransformMutation, + data: ObjectValue, + transformResults: api.Value[] +): ObjectValue { + debugAssert( + transformResults.length === mutation.fieldTransforms.length, + 'TransformResults length mismatch.' + ); + + const builder = new ObjectValueBuilder(data); + for (let i = 0; i < mutation.fieldTransforms.length; i++) { + const fieldTransform = mutation.fieldTransforms[i]; + builder.set(fieldTransform.field, transformResults[i]); + } + return builder.build(); } /** A mutation that deletes the document at the given key. */ @@ -741,58 +824,42 @@ export class DeleteMutation extends Mutation { } readonly type: MutationType = MutationType.Delete; +} - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - this.verifyKeyMatches(maybeDoc); - - debugAssert( - mutationResult.transformResults == null, - 'Transform results received by DeleteMutation.' - ); - - // Unlike applyToLocalView, if we're applying a mutation to a remote - // document the server has accepted the mutation so the precondition must - // have held. - - return new NoDocument(this.key, mutationResult.version, { - hasCommittedMutations: true - }); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - this.verifyKeyMatches(maybeDoc); - - if (!this.precondition.isValidFor(maybeDoc)) { - return maybeDoc; - } - - if (maybeDoc) { - debugAssert( - maybeDoc.key.isEqual(this.key), - 'Can only apply mutation to document with same key' - ); - } - return new NoDocument(this.key, SnapshotVersion.min()); - } +function applyDeleteMutationToRemoteDocument( + mutation: DeleteMutation, + maybeDoc: MaybeDocument | null, + mutationResult: MutationResult +): NoDocument { + debugAssert( + mutationResult.transformResults == null, + 'Transform results received by DeleteMutation.' + ); + + // Unlike applyToLocalView, if we're applying a mutation to a remote + // document the server has accepted the mutation so the precondition must + // have held. + + return new NoDocument(mutation.key, mutationResult.version, { + hasCommittedMutations: true + }); +} - extractBaseValue(maybeDoc: MaybeDocument | null): null { - return null; +function applyDeleteMutationToLocalView( + mutation: DeleteMutation, + maybeDoc: MaybeDocument | null +): MaybeDocument | null { + if (!preconditionIsValidForDocument(mutation.precondition, maybeDoc)) { + return maybeDoc; } - isEqual(other: Mutation): boolean { - return ( - other instanceof DeleteMutation && - this.key.isEqual(other.key) && - this.precondition.isEqual(other.precondition) + if (maybeDoc) { + debugAssert( + maybeDoc.key.isEqual(mutation.key), + 'Can only apply mutation to document with same key' ); } + return new NoDocument(mutation.key, SnapshotVersion.min()); } /** @@ -808,31 +875,4 @@ export class VerifyMutation extends Mutation { } readonly type: MutationType = MutationType.Verify; - - applyToRemoteDocument( - maybeDoc: MaybeDocument | null, - mutationResult: MutationResult - ): MaybeDocument { - fail('VerifyMutation should only be used in Transactions.'); - } - - applyToLocalView( - maybeDoc: MaybeDocument | null, - baseDoc: MaybeDocument | null, - localWriteTime: Timestamp - ): MaybeDocument | null { - fail('VerifyMutation should only be used in Transactions.'); - } - - extractBaseValue(maybeDoc: MaybeDocument | null): null { - fail('VerifyMutation should only be used in Transactions.'); - } - - isEqual(other: Mutation): boolean { - return ( - other instanceof VerifyMutation && - this.key.isEqual(other.key) && - this.precondition.isEqual(other.precondition) - ); - } } diff --git a/packages/firestore/src/model/mutation_batch.ts b/packages/firestore/src/model/mutation_batch.ts index 16044342263..31fcae1583a 100644 --- a/packages/firestore/src/model/mutation_batch.ts +++ b/packages/firestore/src/model/mutation_batch.ts @@ -18,7 +18,7 @@ import { Timestamp } from '../api/timestamp'; import { SnapshotVersion } from '../core/snapshot_version'; import { BatchId } from '../core/types'; -import { hardAssert, debugAssert } from '../util/assert'; +import { debugAssert, hardAssert } from '../util/assert'; import { arrayEquals } from '../util/misc'; import { documentKeySet, @@ -29,7 +29,13 @@ import { } from './collections'; import { MaybeDocument } from './document'; import { DocumentKey } from './document_key'; -import { Mutation, MutationResult } from './mutation'; +import { + applyMutationToLocalView, + applyMutationToRemoteDocument, + Mutation, + mutationEquals, + MutationResult +} from './mutation'; export const BATCHID_UNKNOWN = -1; @@ -91,7 +97,11 @@ export class MutationBatch { const mutation = this.mutations[i]; if (mutation.key.isEqual(docKey)) { const mutationResult = mutationResults[i]; - maybeDoc = mutation.applyToRemoteDocument(maybeDoc, mutationResult); + maybeDoc = applyMutationToRemoteDocument( + mutation, + maybeDoc, + mutationResult + ); } } return maybeDoc; @@ -120,7 +130,8 @@ export class MutationBatch { // transform against a consistent set of values. for (const mutation of this.baseMutations) { if (mutation.key.isEqual(docKey)) { - maybeDoc = mutation.applyToLocalView( + maybeDoc = applyMutationToLocalView( + mutation, maybeDoc, maybeDoc, this.localWriteTime @@ -133,7 +144,8 @@ export class MutationBatch { // Second, apply all user-provided mutations. for (const mutation of this.mutations) { if (mutation.key.isEqual(docKey)) { - maybeDoc = mutation.applyToLocalView( + maybeDoc = applyMutationToLocalView( + mutation, maybeDoc, baseDoc, this.localWriteTime @@ -174,9 +186,11 @@ export class MutationBatch { isEqual(other: MutationBatch): boolean { return ( this.batchId === other.batchId && - arrayEquals(this.mutations, other.mutations, (l, r) => l.isEqual(r)) && + arrayEquals(this.mutations, other.mutations, (l, r) => + mutationEquals(l, r) + ) && arrayEquals(this.baseMutations, other.baseMutations, (l, r) => - l.isEqual(r) + mutationEquals(l, r) ) ); } diff --git a/packages/firestore/src/model/transform_operation.ts b/packages/firestore/src/model/transform_operation.ts index 846c54fd6ad..5c6fc64ee0b 100644 --- a/packages/firestore/src/model/transform_operation.ts +++ b/packages/firestore/src/model/transform_operation.ts @@ -31,155 +31,155 @@ import { serverTimestamp } from './server_timestamps'; import { arrayEquals } from '../util/misc'; /** Represents a transform within a TransformMutation. */ -export interface TransformOperation { - /** - * Computes the local transform result against the provided `previousValue`, - * optionally using the provided localWriteTime. - */ - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value; - - /** - * Computes a final transform result after the transform has been acknowledged - * by the server, potentially using the server-provided transformResult. - */ - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value; - - /** - * If this transform operation is not idempotent, returns the base value to - * persist for this transform. If a base value is returned, the transform - * operation is always applied to this base value, even if document has - * already been updated. - * - * Base values provide consistent behavior for non-idempotent transforms and - * allow us to return the same latency-compensated value even if the backend - * has already applied the transform operation. The base value is null for - * idempotent transforms, as they can be re-played even if the backend has - * already applied them. - * - * @return a base value to store along with the mutation, or null for - * idempotent transforms. - */ - computeBaseValue(previousValue: api.Value | null): api.Value | null; - - isEqual(other: TransformOperation): boolean; +export class TransformOperation { + // Make sure that the structural type of `TransformOperation` is unique. + // See https://github.com/microsoft/TypeScript/issues/5451 + private _ = undefined; } -/** Transforms a value into a server-generated timestamp. */ -export class ServerTimestampTransform implements TransformOperation { - private constructor() {} - static instance = new ServerTimestampTransform(); - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { +/** + * Computes the local transform result against the provided `previousValue`, + * optionally using the provided localWriteTime. + */ +export function applyTransformOperationToLocalView( + transform: TransformOperation, + previousValue: api.Value | null, + localWriteTime: Timestamp +): api.Value { + if (transform instanceof ServerTimestampTransform) { return serverTimestamp(localWriteTime!, previousValue); - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - return transformResult!; - } - - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Server timestamps are idempotent and don't require a base value. - } - - isEqual(other: TransformOperation): boolean { - return other instanceof ServerTimestampTransform; + } else if (transform instanceof ArrayUnionTransformOperation) { + return applyArrayUnionTransformOperation(transform, previousValue); + } else if (transform instanceof ArrayRemoveTransformOperation) { + return applyArrayRemoveTransformOperation(transform, previousValue); + } else { + debugAssert( + transform instanceof NumericIncrementTransformOperation, + 'Expected NumericIncrementTransformOperation but was: ' + transform + ); + return applyNumericIncrementTransformOperationToLocalView( + transform, + previousValue + ); } } -/** Transforms an array value via a union operation. */ -export class ArrayUnionTransformOperation implements TransformOperation { - constructor(readonly elements: api.Value[]) {} - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - return this.apply(previousValue); - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - // The server just sends null as the transform result for array operations, - // so we have to calculate a result the same as we do for local - // applications. - return this.apply(previousValue); - } +/** + * Computes a final transform result after the transform has been acknowledged + * by the server, potentially using the server-provided transformResult. + */ +export function applyTransformOperationToRemoteDocument( + transform: TransformOperation, + previousValue: api.Value | null, + transformResult: api.Value | null +): api.Value { + // The server just sends null as the transform result for array operations, + // so we have to calculate a result the same as we do for local + // applications. + if (transform instanceof ArrayUnionTransformOperation) { + return applyArrayUnionTransformOperation(transform, previousValue); + } else if (transform instanceof ArrayRemoveTransformOperation) { + return applyArrayRemoveTransformOperation(transform, previousValue); + } + + debugAssert( + transformResult !== null, + "Didn't receive transformResult for non-array transform" + ); + return transformResult; +} - private apply(previousValue: api.Value | null): api.Value { - const values = coercedFieldValuesArray(previousValue); - for (const toUnion of this.elements) { - if (!values.some(element => valueEquals(element, toUnion))) { - values.push(toUnion); - } - } - return { arrayValue: { values } }; +/** + * If this transform operation is not idempotent, returns the base value to + * persist for this transform. If a base value is returned, the transform + * operation is always applied to this base value, even if document has + * already been updated. + * + * Base values provide consistent behavior for non-idempotent transforms and + * allow us to return the same latency-compensated value even if the backend + * has already applied the transform operation. The base value is null for + * idempotent transforms, as they can be re-played even if the backend has + * already applied them. + * + * @return a base value to store along with the mutation, or null for + * idempotent transforms. + */ +export function computeTransformOperationBaseValue( + transform: TransformOperation, + previousValue: api.Value | null +): api.Value | null { + if (transform instanceof NumericIncrementTransformOperation) { + return isNumber(previousValue) ? previousValue! : { integerValue: 0 }; } + return null; +} - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Array transforms are idempotent and don't require a base value. +export function transformOperationEquals( + left: TransformOperation, + right: TransformOperation +): boolean { + if ( + left instanceof ArrayUnionTransformOperation && + right instanceof ArrayUnionTransformOperation + ) { + return arrayEquals(left.elements, right.elements, valueEquals); + } else if ( + left instanceof ArrayRemoveTransformOperation && + right instanceof ArrayRemoveTransformOperation + ) { + return arrayEquals(left.elements, right.elements, valueEquals); + } else if ( + left instanceof NumericIncrementTransformOperation && + right instanceof NumericIncrementTransformOperation + ) { + return valueEquals(left.operand, right.operand); } - isEqual(other: TransformOperation): boolean { - return ( - other instanceof ArrayUnionTransformOperation && - arrayEquals(this.elements, other.elements, valueEquals) - ); - } + return ( + left instanceof ServerTimestampTransform && + right instanceof ServerTimestampTransform + ); } -/** Transforms an array value via a remove operation. */ -export class ArrayRemoveTransformOperation implements TransformOperation { - constructor(readonly elements: api.Value[]) {} - - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - return this.apply(previousValue); - } +/** Transforms a value into a server-generated timestamp. */ +export class ServerTimestampTransform extends TransformOperation {} - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - // The server just sends null as the transform result for array operations, - // so we have to calculate a result the same as we do for local - // applications. - return this.apply(previousValue); +/** Transforms an array value via a union operation. */ +export class ArrayUnionTransformOperation extends TransformOperation { + constructor(readonly elements: api.Value[]) { + super(); } +} - private apply(previousValue: api.Value | null): api.Value { - let values = coercedFieldValuesArray(previousValue); - for (const toRemove of this.elements) { - values = values.filter(element => !valueEquals(element, toRemove)); +function applyArrayUnionTransformOperation( + transform: ArrayUnionTransformOperation, + previousValue: api.Value | null +): api.Value { + const values = coercedFieldValuesArray(previousValue); + for (const toUnion of transform.elements) { + if (!values.some(element => valueEquals(element, toUnion))) { + values.push(toUnion); } - return { arrayValue: { values } }; } + return { arrayValue: { values } }; +} - computeBaseValue(previousValue: api.Value | null): api.Value | null { - return null; // Array transforms are idempotent and don't require a base value. +/** Transforms an array value via a remove operation. */ +export class ArrayRemoveTransformOperation extends TransformOperation { + constructor(readonly elements: api.Value[]) { + super(); } +} - isEqual(other: TransformOperation): boolean { - return ( - other instanceof ArrayRemoveTransformOperation && - arrayEquals(this.elements, other.elements, valueEquals) - ); +function applyArrayRemoveTransformOperation( + transform: ArrayRemoveTransformOperation, + previousValue: api.Value | null +): api.Value { + let values = coercedFieldValuesArray(previousValue); + for (const toRemove of transform.elements) { + values = values.filter(element => !valueEquals(element, toRemove)); } + return { arrayValue: { values } }; } /** @@ -188,62 +188,40 @@ export class ArrayRemoveTransformOperation implements TransformOperation { * backend does not cap integer values at 2^63. Instead, JavaScript number * arithmetic is used and precision loss can occur for values greater than 2^53. */ -export class NumericIncrementTransformOperation implements TransformOperation { +export class NumericIncrementTransformOperation extends TransformOperation { constructor( - private readonly serializer: JsonProtoSerializer, + readonly serializer: JsonProtoSerializer, readonly operand: api.Value ) { + super(); debugAssert( isNumber(operand), 'NumericIncrementTransform transform requires a NumberValue' ); } +} - applyToLocalView( - previousValue: api.Value | null, - localWriteTime: Timestamp - ): api.Value { - // PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit - // precision and resolves overflows by reducing precision, we do not - // manually cap overflows at 2^63. - const baseValue = this.computeBaseValue(previousValue); - const sum = this.asNumber(baseValue) + this.asNumber(this.operand); - if (isInteger(baseValue) && isInteger(this.operand)) { - return toInteger(sum); - } else { - return toDouble(this.serializer, sum); - } - } - - applyToRemoteDocument( - previousValue: api.Value | null, - transformResult: api.Value | null - ): api.Value { - debugAssert( - transformResult !== null, - "Didn't receive transformResult for NUMERIC_ADD transform" - ); - return transformResult; - } - - /** - * Inspects the provided value, returning the provided value if it is already - * a NumberValue, otherwise returning a coerced value of 0. - */ - computeBaseValue(previousValue: api.Value | null): api.Value { - return isNumber(previousValue) ? previousValue! : { integerValue: 0 }; - } - - isEqual(other: TransformOperation): boolean { - return ( - other instanceof NumericIncrementTransformOperation && - valueEquals(this.operand, other.operand) - ); +export function applyNumericIncrementTransformOperationToLocalView( + transform: NumericIncrementTransformOperation, + previousValue: api.Value | null +): api.Value { + // PORTING NOTE: Since JavaScript's integer arithmetic is limited to 53 bit + // precision and resolves overflows by reducing precision, we do not + // manually cap overflows at 2^63. + const baseValue = computeTransformOperationBaseValue( + transform, + previousValue + )!; + const sum = asNumber(baseValue) + asNumber(transform.operand); + if (isInteger(baseValue) && isInteger(transform.operand)) { + return toInteger(sum); + } else { + return toDouble(transform.serializer, sum); } +} - private asNumber(value: api.Value): number { - return normalizeNumber(value.integerValue || value.doubleValue); - } +function asNumber(value: api.Value): number { + return normalizeNumber(value.integerValue || value.doubleValue); } function coercedFieldValuesArray(value: api.Value | null): api.Value[] { diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 3e0dd5e7182..f5a9117b656 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -774,7 +774,7 @@ function fromFieldTransform( proto.setToServerValue === 'REQUEST_TIME', 'Unknown server value transform proto: ' + JSON.stringify(proto) ); - transform = ServerTimestampTransform.instance; + transform = new ServerTimestampTransform(); } else if ('appendMissingElements' in proto) { const values = proto.appendMissingElements!.values || []; transform = new ArrayUnionTransformOperation(values); diff --git a/packages/firestore/test/unit/model/mutation.test.ts b/packages/firestore/test/unit/model/mutation.test.ts index 10320234529..8c53e9e45de 100644 --- a/packages/firestore/test/unit/model/mutation.test.ts +++ b/packages/firestore/test/unit/model/mutation.test.ts @@ -21,6 +21,9 @@ import { Timestamp } from '../../../src/api/timestamp'; import { Document, MaybeDocument } from '../../../src/model/document'; import { serverTimestamp } from '../../../src/model/server_timestamps'; import { + applyMutationToLocalView, + applyMutationToRemoteDocument, + extractMutationBaseValue, Mutation, MutationResult, Precondition @@ -58,7 +61,7 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, docData); const set = setMutation('collection/key', { bar: 'bar-value' }); - const setDoc = set.applyToLocalView(baseDoc, baseDoc, timestamp); + const setDoc = applyMutationToLocalView(set, baseDoc, baseDoc, timestamp); expect(setDoc).to.deep.equal( doc( 'collection/key', @@ -77,7 +80,12 @@ describe('Mutation', () => { 'foo.bar': 'new-bar-value' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -98,7 +106,12 @@ describe('Mutation', () => { Precondition.none() ); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -119,7 +132,12 @@ describe('Mutation', () => { Precondition.none() ); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -138,7 +156,12 @@ describe('Mutation', () => { 'foo.bar': FieldValue.delete() }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -158,7 +181,12 @@ describe('Mutation', () => { 'foo.bar': 'new-bar-value' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -172,7 +200,12 @@ describe('Mutation', () => { it('patching a NoDocument yields a NoDocument', () => { const baseDoc = deletedDoc('collection/key', 0); const patch = patchMutation('collection/key', { foo: 'bar' }); - const patchedDoc = patch.applyToLocalView(baseDoc, baseDoc, timestamp); + const patchedDoc = applyMutationToLocalView( + patch, + baseDoc, + baseDoc, + timestamp + ); expect(patchedDoc).to.deep.equal(baseDoc); }); @@ -184,7 +217,8 @@ describe('Mutation', () => { 'foo.bar': FieldValue.serverTimestamp() }); - const transformedDoc = transform.applyToLocalView( + const transformedDoc = applyMutationToLocalView( + transform, baseDoc, baseDoc, timestamp @@ -360,7 +394,8 @@ describe('Mutation', () => { for (const transformData of transforms) { const transform = transformMutation('collection/key', transformData); - transformedDoc = transform.applyToLocalView( + transformedDoc = applyMutationToLocalView( + transform, transformedDoc, transformedDoc, timestamp @@ -389,7 +424,8 @@ describe('Mutation', () => { } } ]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -414,7 +450,8 @@ describe('Mutation', () => { // Server just sends null transform results for array operations. const mutationResult = new MutationResult(version(1), [null, null]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -500,7 +537,8 @@ describe('Mutation', () => { const mutationResult = new MutationResult(version(1), [ { integerValue: 3 } ]); - const transformedDoc = transform.applyToRemoteDocument( + const transformedDoc = applyMutationToRemoteDocument( + transform, baseDoc, mutationResult ); @@ -514,7 +552,12 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, { foo: 'bar' }); const mutation = deleteMutation('collection/key'); - const result = mutation.applyToLocalView(baseDoc, baseDoc, Timestamp.now()); + const result = applyMutationToLocalView( + mutation, + baseDoc, + baseDoc, + Timestamp.now() + ); expect(result).to.deep.equal(deletedDoc('collection/key', 0)); }); @@ -523,7 +566,7 @@ describe('Mutation', () => { const docSet = setMutation('collection/key', { foo: 'new-bar' }); const setResult = mutationResult(4); - const setDoc = docSet.applyToRemoteDocument(baseDoc, setResult); + const setDoc = applyMutationToRemoteDocument(docSet, baseDoc, setResult); expect(setDoc).to.deep.equal( doc( 'collection/key', @@ -539,7 +582,7 @@ describe('Mutation', () => { const mutation = patchMutation('collection/key', { foo: 'new-bar' }); const result = mutationResult(5); - const patchedDoc = mutation.applyToRemoteDocument(baseDoc, result); + const patchedDoc = applyMutationToRemoteDocument(mutation, baseDoc, result); expect(patchedDoc).to.deep.equal( doc( 'collection/key', @@ -558,7 +601,11 @@ describe('Mutation', () => { mutationResult: MutationResult, expected: MaybeDocument | null ): void { - const actual = mutation.applyToRemoteDocument(base, mutationResult); + const actual = applyMutationToRemoteDocument( + mutation, + base, + mutationResult + ); expect(actual).to.deep.equal(expected); } @@ -614,13 +661,13 @@ describe('Mutation', () => { const baseDoc = doc('collection/key', 0, data); const set = setMutation('collection/key', { foo: 'bar' }); - expect(set.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(set, baseDoc)).to.be.null; const patch = patchMutation('collection/key', { foo: 'bar' }); - expect(patch.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(patch, baseDoc)).to.be.null; const deleter = deleteMutation('collection/key'); - expect(deleter.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(deleter, baseDoc)).to.be.null; }); it('extracts null base value for ServerTimestamp', () => { @@ -634,7 +681,7 @@ describe('Mutation', () => { // Server timestamps are idempotent and don't have base values. const transform = transformMutation('collection/key', allTransforms); - expect(transform.extractBaseValue(baseDoc)).to.be.null; + expect(extractMutationBaseValue(transform, baseDoc)).to.be.null; }); it('extracts base value for increment', () => { @@ -672,7 +719,7 @@ describe('Mutation', () => { missing: 0, nested: { double: 42.0, long: 42, text: 0, map: 0, missing: 0 } }); - const actualBaseValue = transform.extractBaseValue(baseDoc); + const actualBaseValue = extractMutationBaseValue(transform, baseDoc); expect(expectedBaseValue.isEqual(actualBaseValue!)).to.be.true; }); @@ -683,12 +730,14 @@ describe('Mutation', () => { const increment = { sum: FieldValue.increment(1) }; const transform = transformMutation('collection/key', increment); - let mutatedDoc = transform.applyToLocalView( + let mutatedDoc = applyMutationToLocalView( + transform, baseDoc, baseDoc, Timestamp.now() ); - mutatedDoc = transform.applyToLocalView( + mutatedDoc = applyMutationToLocalView( + transform, mutatedDoc, baseDoc, Timestamp.now() diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index 859e08c8819..376f7ab0174 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -42,6 +42,7 @@ import { DeleteMutation, FieldMask, Mutation, + mutationEquals, Precondition, SetMutation, VerifyMutation @@ -601,12 +602,10 @@ export function serializerTest( }); describe('toMutation / fromMutation', () => { - addEqualityMatcher(); - function verifyMutation(mutation: Mutation, proto: unknown): void { const serialized = toMutation(s, mutation); expect(serialized).to.deep.equal(proto); - expect(fromMutation(s, serialized)).to.deep.equal(mutation); + expect(mutationEquals(fromMutation(s, serialized), mutation)); } it('SetMutation', () => { From b98825ba8de28727a62c54f6aabef8de3a8b8e3c Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 30 Jun 2020 22:44:14 -0700 Subject: [PATCH 2/5] Make UserDataReader tree-shakeable --- packages/firestore/exp/src/api/reference.ts | 22 +- packages/firestore/lite/src/api/reference.ts | 19 +- .../firestore/lite/src/api/transaction.ts | 16 +- .../firestore/lite/src/api/write_batch.ts | 12 +- .../firestore/lite/test/dependencies.json | 208 ++------- packages/firestore/src/api/database.ts | 39 +- .../firestore/src/api/user_data_reader.ts | 394 +++++++++--------- .../test/unit/remote/serializer.helper.ts | 21 +- packages/firestore/test/util/helpers.ts | 14 +- 9 files changed, 341 insertions(+), 404 deletions(-) diff --git a/packages/firestore/exp/src/api/reference.ts b/packages/firestore/exp/src/api/reference.ts index 3f1bcdea6b0..7085cefdd6e 100644 --- a/packages/firestore/exp/src/api/reference.ts +++ b/packages/firestore/exp/src/api/reference.ts @@ -22,15 +22,18 @@ import * as firestore from '../../index'; import { Firestore } from './database'; import { DocumentKeyReference, - ParsedUpdateData + ParsedUpdateData, + parseSetData, + parseUpdateData, + parseUpdateVarargs } from '../../../src/api/user_data_reader'; import { debugAssert } from '../../../src/util/assert'; import { cast } from '../../../lite/src/api/util'; import { DocumentSnapshot, QuerySnapshot } from './snapshot'; import { addDocSnapshotListener, - addSnapshotsInSyncListener, addQuerySnapshotListener, + addSnapshotsInSyncListener, applyFirestoreDataConverter, getDocsViaSnapshotListener, getDocViaSnapshotListener, @@ -175,7 +178,8 @@ export function setDoc( options ); const dataReader = newUserDataReader(firestore); - const parsed = dataReader.parseSetData( + const parsed = parseSetData( + dataReader, 'setDoc', ref._key, convertedValue, @@ -215,7 +219,8 @@ export function updateDoc( typeof fieldOrUpdateData === 'string' || fieldOrUpdateData instanceof FieldPath ) { - parsed = dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + dataReader, 'updateDoc', ref._key, fieldOrUpdateData, @@ -223,7 +228,8 @@ export function updateDoc( moreFieldsAndValues ); } else { - parsed = dataReader.parseUpdateData( + parsed = parseUpdateData( + dataReader, 'updateDoc', ref._key, fieldOrUpdateData @@ -262,11 +268,13 @@ export function addDoc( const convertedValue = applyFirestoreDataConverter(collRef._converter, data); const dataReader = newUserDataReader(collRef.firestore); - const parsed = dataReader.parseSetData( + const parsed = parseSetData( + dataReader, 'addDoc', docRef._key, convertedValue, - collRef._converter !== null + collRef._converter !== null, + {} ); return firestore diff --git a/packages/firestore/lite/src/api/reference.ts b/packages/firestore/lite/src/api/reference.ts index 845ab52faa4..08a314ec000 100644 --- a/packages/firestore/lite/src/api/reference.ts +++ b/packages/firestore/lite/src/api/reference.ts @@ -23,6 +23,9 @@ import { Firestore } from './database'; import { DocumentKeyReference, ParsedUpdateData, + parseSetData, + parseUpdateData, + parseUpdateVarargs, UserDataReader } from '../../../src/api/user_data_reader'; import { @@ -35,9 +38,9 @@ import { ResourcePath } from '../../../src/model/path'; import { AutoId } from '../../../src/util/misc'; import { DocumentSnapshot, + fieldPathFromArgument, QueryDocumentSnapshot, - QuerySnapshot, - fieldPathFromArgument + QuerySnapshot } from './snapshot'; import { invokeBatchGetDocumentsRpc, @@ -464,7 +467,8 @@ export function setDoc( options ); const dataReader = newUserDataReader(ref.firestore); - const parsed = dataReader.parseSetData( + const parsed = parseSetData( + dataReader, 'setDoc', ref._key, convertedValue, @@ -506,7 +510,8 @@ export function updateDoc( typeof fieldOrUpdateData === 'string' || fieldOrUpdateData instanceof FieldPath ) { - parsed = dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + dataReader, 'updateDoc', ref._key, fieldOrUpdateData, @@ -514,7 +519,8 @@ export function updateDoc( moreFieldsAndValues ); } else { - parsed = dataReader.parseUpdateData( + parsed = parseUpdateData( + dataReader, 'updateDoc', ref._key, fieldOrUpdateData @@ -554,7 +560,8 @@ export function addDoc( const convertedValue = applyFirestoreDataConverter(collRef._converter, data); const dataReader = newUserDataReader(collRef.firestore); - const parsed = dataReader.parseSetData( + const parsed = parseSetData( + dataReader, 'addDoc', docRef._key, convertedValue, diff --git a/packages/firestore/lite/src/api/transaction.ts b/packages/firestore/lite/src/api/transaction.ts index 6ee7e60f5ae..aa7a8ca0793 100644 --- a/packages/firestore/lite/src/api/transaction.ts +++ b/packages/firestore/lite/src/api/transaction.ts @@ -17,7 +17,12 @@ import * as firestore from '../../'; -import { UserDataReader } from '../../../src/api/user_data_reader'; +import { + parseSetData, + parseUpdateData, + parseUpdateVarargs, + UserDataReader +} from '../../../src/api/user_data_reader'; import { Transaction as InternalTransaction } from '../../../src/core/transaction'; import { Document, @@ -100,7 +105,8 @@ export class Transaction implements firestore.Transaction { value, options ); - const parsed = this._dataReader.parseSetData( + const parsed = parseSetData( + this._dataReader, 'Transaction.set', ref._key, convertedValue, @@ -134,7 +140,8 @@ export class Transaction implements firestore.Transaction { typeof fieldOrUpdateData === 'string' || fieldOrUpdateData instanceof FieldPath ) { - parsed = this._dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData, @@ -142,7 +149,8 @@ export class Transaction implements firestore.Transaction { moreFieldsAndValues ); } else { - parsed = this._dataReader.parseUpdateData( + parsed = parseUpdateData( + this._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData diff --git a/packages/firestore/lite/src/api/write_batch.ts b/packages/firestore/lite/src/api/write_batch.ts index 5f7f6e824bb..2eead98ec12 100644 --- a/packages/firestore/lite/src/api/write_batch.ts +++ b/packages/firestore/lite/src/api/write_batch.ts @@ -25,6 +25,9 @@ import { Code, FirestoreError } from '../../../src/util/error'; import { applyFirestoreDataConverter } from '../../../src/api/database'; import { DocumentKeyReference, + parseSetData, + parseUpdateData, + parseUpdateVarargs, UserDataReader } from '../../../src/api/user_data_reader'; import { cast } from './util'; @@ -67,7 +70,8 @@ export class WriteBatch implements firestore.WriteBatch { value, options ); - const parsed = this._dataReader.parseSetData( + const parsed = parseSetData( + this._dataReader, 'WriteBatch.set', ref._key, convertedValue, @@ -105,7 +109,8 @@ export class WriteBatch implements firestore.WriteBatch { typeof fieldOrUpdateData === 'string' || fieldOrUpdateData instanceof FieldPath ) { - parsed = this._dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData, @@ -113,7 +118,8 @@ export class WriteBatch implements firestore.WriteBatch { moreFieldsAndValues ); } else { - parsed = this._dataReader.parseUpdateData( + parsed = parseUpdateData( + this._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData diff --git a/packages/firestore/lite/test/dependencies.json b/packages/firestore/lite/test/dependencies.json index 41462a2b7c1..d3b0bcf1061 100644 --- a/packages/firestore/lite/test/dependencies.json +++ b/packages/firestore/lite/test/dependencies.json @@ -99,8 +99,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -117,7 +115,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -151,6 +148,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -198,13 +196,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -218,33 +214,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91030 + "sizeInBytes": 81798 }, "DocumentReference": { "dependencies": { @@ -605,8 +592,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "forEach", @@ -621,7 +606,6 @@ "invalidClassError", "isArray", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullValue", @@ -652,6 +636,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -693,13 +678,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -713,31 +696,22 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 77344 + "sizeInBytes": 68112 }, "QueryDocumentSnapshot": { "dependencies": { @@ -981,6 +955,9 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", + "parseUpdateData", + "parseUpdateVarargs", "primitiveComparator", "registerFirestore", "terminate", @@ -1055,7 +1032,7 @@ ], "variables": [] }, - "sizeInBytes": 62673 + "sizeInBytes": 62454 }, "WriteBatch": { "dependencies": { @@ -1117,6 +1094,9 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", + "parseUpdateData", + "parseUpdateVarargs", "primitiveComparator", "registerFirestore", "terminate", @@ -1185,7 +1165,7 @@ ], "variables": [] }, - "sizeInBytes": 56525 + "sizeInBytes": 56306 }, "addDoc": { "dependencies": { @@ -1231,7 +1211,6 @@ "errorMessage", "fail", "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -1287,8 +1266,10 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", "primitiveComparator", "randomBytes", "refValue", @@ -1346,7 +1327,6 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DeleteMutation", "DocumentKey", "DocumentKeyReference", @@ -1371,11 +1351,9 @@ "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", "ParsedSetData", - "ParsedUpdateData", "PatchMutation", "Precondition", "Query", @@ -1397,7 +1375,7 @@ ], "variables": [] }, - "sizeInBytes": 97301 + "sizeInBytes": 92272 }, "arrayRemove": { "dependencies": { @@ -1632,8 +1610,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -1650,7 +1626,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -1684,6 +1659,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -1732,13 +1708,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -1752,33 +1726,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91658 + "sizeInBytes": 82426 }, "collectionGroup": { "dependencies": { @@ -1821,8 +1786,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -1839,7 +1802,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -1873,6 +1835,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -1919,13 +1882,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -1939,33 +1900,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91090 + "sizeInBytes": 81858 }, "deleteDoc": { "dependencies": { @@ -2133,8 +2085,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -2151,7 +2101,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -2185,6 +2134,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -2235,13 +2185,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -2255,33 +2203,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 92479 + "sizeInBytes": 83247 }, "documentId": { "dependencies": { @@ -2531,8 +2470,6 @@ "errorMessage", "extractLocalPathFromResourceName", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "forEach", @@ -2587,6 +2524,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -2640,14 +2578,12 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "Document", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -2662,33 +2598,25 @@ "KeyFieldFilter", "KeyFieldInFilter", "MaybeDocument", - "Mutation", "OAuthToken", "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query$1", "QueryDocumentSnapshot", "QuerySnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "SnapshotVersion", "StreamBridge", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 84898 + "sizeInBytes": 76293 }, "increment": { "dependencies": { @@ -2873,8 +2801,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -2891,7 +2817,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -2926,6 +2851,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -2973,13 +2899,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -2993,33 +2917,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91373 + "sizeInBytes": 82141 }, "queryEqual": { "dependencies": { @@ -3046,8 +2961,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "forEach", @@ -3062,7 +2975,6 @@ "invalidClassError", "isArray", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullValue", @@ -3093,6 +3005,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -3135,13 +3048,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -3155,31 +3066,22 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 77548 + "sizeInBytes": 68316 }, "refEqual": { "dependencies": { @@ -3221,8 +3123,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "filterEquals", @@ -3239,7 +3139,6 @@ "isArray", "isDocumentTarget", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullOrUndefined", @@ -3273,6 +3172,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -3321,13 +3221,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -3341,33 +3239,24 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query", "Query$1", "QueryDocumentSnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "TargetImpl", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 91315 + "sizeInBytes": 82083 }, "runTransaction": { "dependencies": { @@ -3452,6 +3341,9 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", + "parseUpdateData", + "parseUpdateVarargs", "primitiveComparator", "registerFirestore", "runTransaction", @@ -3547,7 +3439,7 @@ ], "variables": [] }, - "sizeInBytes": 81867 + "sizeInBytes": 81648 }, "serverTimestamp": { "dependencies": { @@ -3620,7 +3512,6 @@ "errorMessage", "fail", "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromDotSeparatedString", "forEach", "formatJSON", @@ -3666,6 +3557,7 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", "primitiveComparator", "registerFirestore", "setDoc", @@ -3708,7 +3600,6 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DeleteMutation", "DocumentKeyReference", "DocumentReference", @@ -3725,10 +3616,8 @@ "NumericIncrementTransformOperation", "OAuthToken", "ObjectValue", - "ObjectValueBuilder", "ParseContext", "ParsedSetData", - "ParsedUpdateData", "PatchMutation", "Precondition", "ResourcePath", @@ -3745,7 +3634,7 @@ ], "variables": [] }, - "sizeInBytes": 58711 + "sizeInBytes": 53321 }, "setLogLevel": { "dependencies": { @@ -3817,8 +3706,6 @@ "encodeBase64", "errorMessage", "fail", - "fieldMaskContains", - "fieldPathFromArgument", "fieldPathFromArgument$1", "fieldPathFromDotSeparatedString", "forEach", @@ -3833,7 +3720,6 @@ "invalidClassError", "isArray", "isEmpty", - "isMapValue", "isNanValue", "isNegativeZero", "isNullValue", @@ -3864,6 +3750,7 @@ "parseArray", "parseData", "parseObject", + "parseQueryValue", "parseScalarValue", "parseSentinelFieldValue", "primitiveComparator", @@ -3907,13 +3794,11 @@ "Datastore", "DatastoreImpl", "Deferred", - "DeleteFieldValueImpl", "DocumentKey", "DocumentKeyReference", "DocumentReference", "DocumentSnapshot", "FieldFilter", - "FieldMask", "FieldPath", "FieldPath$1", "FieldPath$2", @@ -3927,32 +3812,23 @@ "JsonProtoSerializer", "KeyFieldFilter", "KeyFieldInFilter", - "Mutation", "OAuthToken", - "ObjectValue", - "ObjectValueBuilder", "OrderBy", "ParseContext", - "ParsedSetData", - "ParsedUpdateData", - "PatchMutation", - "Precondition", "Query$1", "QueryDocumentSnapshot", "QuerySnapshot", "ResourcePath", "SerializableFieldValue", - "SetMutation", "StreamBridge", "Timestamp", - "TransformMutation", "User", "UserDataReader", "UserDataWriter" ], "variables": [] }, - "sizeInBytes": 78281 + "sizeInBytes": 69049 }, "terminate": { "dependencies": { @@ -4062,6 +3938,8 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseUpdateData", + "parseUpdateVarargs", "primitiveComparator", "registerFirestore", "terminate", @@ -4124,7 +4002,6 @@ "ObjectValue", "ObjectValueBuilder", "ParseContext", - "ParsedSetData", "ParsedUpdateData", "PatchMutation", "Precondition", @@ -4142,7 +4019,7 @@ ], "variables": [] }, - "sizeInBytes": 58748 + "sizeInBytes": 56642 }, "writeBatch": { "dependencies": { @@ -4209,6 +4086,9 @@ "parseObject", "parseScalarValue", "parseSentinelFieldValue", + "parseSetData", + "parseUpdateData", + "parseUpdateVarargs", "primitiveComparator", "registerFirestore", "terminate", @@ -4291,6 +4171,6 @@ ], "variables": [] }, - "sizeInBytes": 60620 + "sizeInBytes": 60401 } } \ No newline at end of file diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 9625ac3086c..d6e8d719aeb 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -90,6 +90,10 @@ import { import { DocumentKeyReference, fieldPathFromArgument, + parseQueryValue, + parseSetData, + parseUpdateData, + parseUpdateVarargs, UntypedFirestoreDataConverter, UserDataReader } from './user_data_reader'; @@ -754,7 +758,8 @@ export class Transaction implements firestore.Transaction { value, options ); - const parsed = this._firestore._dataReader.parseSetData( + const parsed = parseSetData( + this._firestore._dataReader, 'Transaction.set', ref._key, convertedValue, @@ -794,7 +799,8 @@ export class Transaction implements firestore.Transaction { documentRef, this._firestore ); - parsed = this._firestore._dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + this._firestore._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData, @@ -808,7 +814,8 @@ export class Transaction implements firestore.Transaction { documentRef, this._firestore ); - parsed = this._firestore._dataReader.parseUpdateData( + parsed = parseUpdateData( + this._firestore._dataReader, 'Transaction.update', ref._key, fieldOrUpdateData @@ -861,7 +868,8 @@ export class WriteBatch implements firestore.WriteBatch { value, options ); - const parsed = this._firestore._dataReader.parseSetData( + const parsed = parseSetData( + this._firestore._dataReader, 'WriteBatch.set', ref._key, convertedValue, @@ -905,7 +913,8 @@ export class WriteBatch implements firestore.WriteBatch { documentRef, this._firestore ); - parsed = this._firestore._dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + this._firestore._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData, @@ -919,7 +928,8 @@ export class WriteBatch implements firestore.WriteBatch { documentRef, this._firestore ); - parsed = this._firestore._dataReader.parseUpdateData( + parsed = parseUpdateData( + this._firestore._dataReader, 'WriteBatch.update', ref._key, fieldOrUpdateData @@ -1061,7 +1071,8 @@ export class DocumentReference value, options ); - const parsed = this.firestore._dataReader.parseSetData( + const parsed = parseSetData( + this.firestore._dataReader, 'DocumentReference.set', this._key, convertedValue, @@ -1091,7 +1102,8 @@ export class DocumentReference fieldOrUpdateData instanceof ExternalFieldPath ) { validateAtLeastNumberOfArgs('DocumentReference.update', arguments, 2); - parsed = this.firestore._dataReader.parseUpdateVarargs( + parsed = parseUpdateVarargs( + this.firestore._dataReader, 'DocumentReference.update', this._key, fieldOrUpdateData, @@ -1100,7 +1112,8 @@ export class DocumentReference ); } else { validateExactNumberOfArgs('DocumentReference.update', arguments, 1); - parsed = this.firestore._dataReader.parseUpdateData( + parsed = parseUpdateData( + this.firestore._dataReader, 'DocumentReference.update', this._key, fieldOrUpdateData @@ -1545,11 +1558,11 @@ export class BaseQuery { if (op === Operator.IN || op === Operator.ARRAY_CONTAINS_ANY) { this.validateDisjunctiveFilterElements(value, op); } - fieldValue = this._dataReader.parseQueryValue( + fieldValue = parseQueryValue( + this._dataReader, 'Query.where', value, - // We only allow nested arrays for IN queries. - /** allowArrays = */ op === Operator.IN + op === Operator.IN ); } const filter = FieldFilter.create(fieldPath, op, fieldValue); @@ -1695,7 +1708,7 @@ export class BaseQuery { const key = new DocumentKey(path); components.push(refValue(this._databaseId, key)); } else { - const wrapped = this._dataReader.parseQueryValue(methodName, rawValue); + const wrapped = parseQueryValue(this._dataReader, methodName, rawValue); components.push(wrapped); } } diff --git a/packages/firestore/src/api/user_data_reader.ts b/packages/firestore/src/api/user_data_reader.ts index 71abaeb5bec..03b864ddc98 100644 --- a/packages/firestore/src/api/user_data_reader.ts +++ b/packages/firestore/src/api/user_data_reader.ts @@ -319,97 +319,188 @@ export class UserDataReader { this.serializer = serializer || newSerializer(databaseId); } - /** Parse document data from a set() call. */ - parseSetData( + /** Creates a new top-level parse context. */ + createContext( + dataSource: UserDataSource, methodName: string, - targetDoc: DocumentKey, - input: unknown, - hasConverter: boolean, - options: firestore.SetOptions = {} - ): ParsedSetData { - const context = this.createContext( - options.merge || options.mergeFields - ? UserDataSource.MergeSet - : UserDataSource.Set, - methodName, - targetDoc, - hasConverter + targetDoc?: DocumentKey, + hasConverter = false + ): ParseContext { + return new ParseContext( + { + dataSource, + methodName, + targetDoc, + path: FieldPath.emptyPath(), + arrayElement: false, + hasConverter + }, + this.databaseId, + this.serializer, + this.ignoreUndefinedProperties ); - validatePlainObject('Data must be an object, but it was:', context, input); - const updateData = parseObject(input, context)!; - - let fieldMask: FieldMask | null; - let fieldTransforms: FieldTransform[]; - - if (options.merge) { - fieldMask = new FieldMask(context.fieldMask); - fieldTransforms = context.fieldTransforms; - } else if (options.mergeFields) { - const validatedFieldPaths: FieldPath[] = []; - - for (const stringOrFieldPath of options.mergeFields) { - let fieldPath: FieldPath; - - if (stringOrFieldPath instanceof BaseFieldPath) { - fieldPath = stringOrFieldPath._internalPath; - } else if (typeof stringOrFieldPath === 'string') { - fieldPath = fieldPathFromDotSeparatedString( - methodName, - stringOrFieldPath, - targetDoc - ); - } else { - throw fail( - 'Expected stringOrFieldPath to be a string or a FieldPath' - ); - } + } +} - if (!context.contains(fieldPath)) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - `Field '${fieldPath}' is specified in your field mask but missing from your input data.` - ); - } +/** Parse document data from a set() call. */ +export function parseSetData( + userDataReader: UserDataReader, + methodName: string, + targetDoc: DocumentKey, + input: unknown, + hasConverter: boolean, + options: firestore.SetOptions = {} +): ParsedSetData { + const context = userDataReader.createContext( + options.merge || options.mergeFields + ? UserDataSource.MergeSet + : UserDataSource.Set, + methodName, + targetDoc, + hasConverter + ); + validatePlainObject('Data must be an object, but it was:', context, input); + const updateData = parseObject(input, context)!; - if (!fieldMaskContains(validatedFieldPaths, fieldPath)) { - validatedFieldPaths.push(fieldPath); - } + let fieldMask: FieldMask | null; + let fieldTransforms: FieldTransform[]; + + if (options.merge) { + fieldMask = new FieldMask(context.fieldMask); + fieldTransforms = context.fieldTransforms; + } else if (options.mergeFields) { + const validatedFieldPaths: FieldPath[] = []; + + for (const stringOrFieldPath of options.mergeFields) { + let fieldPath: FieldPath; + + if (stringOrFieldPath instanceof BaseFieldPath) { + fieldPath = stringOrFieldPath._internalPath; + } else if (typeof stringOrFieldPath === 'string') { + fieldPath = fieldPathFromDotSeparatedString( + methodName, + stringOrFieldPath, + targetDoc + ); + } else { + throw fail('Expected stringOrFieldPath to be a string or a FieldPath'); } - fieldMask = new FieldMask(validatedFieldPaths); - fieldTransforms = context.fieldTransforms.filter(transform => - fieldMask!.covers(transform.field) - ); + if (!context.contains(fieldPath)) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Field '${fieldPath}' is specified in your field mask but missing from your input data.` + ); + } + + if (!fieldMaskContains(validatedFieldPaths, fieldPath)) { + validatedFieldPaths.push(fieldPath); + } + } + + fieldMask = new FieldMask(validatedFieldPaths); + fieldTransforms = context.fieldTransforms.filter(transform => + fieldMask!.covers(transform.field) + ); + } else { + fieldMask = null; + fieldTransforms = context.fieldTransforms; + } + + return new ParsedSetData( + new ObjectValue(updateData), + fieldMask, + fieldTransforms + ); +} + +/** Parse update data from an update() call. */ +export function parseUpdateData( + userDataReader: UserDataReader, + methodName: string, + targetDoc: DocumentKey, + input: unknown +): ParsedUpdateData { + const context = userDataReader.createContext( + UserDataSource.Update, + methodName, + targetDoc + ); + validatePlainObject('Data must be an object, but it was:', context, input); + + const fieldMaskPaths: FieldPath[] = []; + const updateData = new ObjectValueBuilder(); + forEach(input as Dict, (key, value) => { + const path = fieldPathFromDotSeparatedString(methodName, key, targetDoc); + + const childContext = context.childContextForFieldPath(path); + if ( + value instanceof SerializableFieldValue && + value._delegate instanceof DeleteFieldValueImpl + ) { + // Add it to the field mask, but don't add anything to updateData. + fieldMaskPaths.push(path); } else { - fieldMask = null; - fieldTransforms = context.fieldTransforms; + const parsedValue = parseData(value, childContext); + if (parsedValue != null) { + fieldMaskPaths.push(path); + updateData.set(path, parsedValue); + } } + }); - return new ParsedSetData( - new ObjectValue(updateData), - fieldMask, - fieldTransforms + const mask = new FieldMask(fieldMaskPaths); + return new ParsedUpdateData( + updateData.build(), + mask, + context.fieldTransforms + ); +} + +/** Parse update data from a list of field/value arguments. */ +export function parseUpdateVarargs( + userDataReader: UserDataReader, + methodName: string, + targetDoc: DocumentKey, + field: string | BaseFieldPath, + value: unknown, + moreFieldsAndValues: unknown[] +): ParsedUpdateData { + const context = userDataReader.createContext( + UserDataSource.Update, + methodName, + targetDoc + ); + const keys = [fieldPathFromArgument(methodName, field, targetDoc)]; + const values = [value]; + + if (moreFieldsAndValues.length % 2 !== 0) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Function ${methodName}() needs to be called with an even number ` + + 'of arguments that alternate between field names and values.' ); } - /** Parse update data from an update() call. */ - parseUpdateData( - methodName: string, - targetDoc: DocumentKey, - input: unknown - ): ParsedUpdateData { - const context = this.createContext( - UserDataSource.Update, - methodName, - targetDoc + for (let i = 0; i < moreFieldsAndValues.length; i += 2) { + keys.push( + fieldPathFromArgument( + methodName, + moreFieldsAndValues[i] as string | BaseFieldPath + ) ); - validatePlainObject('Data must be an object, but it was:', context, input); + values.push(moreFieldsAndValues[i + 1]); + } - const fieldMaskPaths: FieldPath[] = []; - const updateData = new ObjectValueBuilder(); - forEach(input as Dict, (key, value) => { - const path = fieldPathFromDotSeparatedString(methodName, key, targetDoc); + const fieldMaskPaths: FieldPath[] = []; + const updateData = new ObjectValueBuilder(); + // We iterate in reverse order to pick the last value for a field if the + // user specified the field multiple times. + for (let i = keys.length - 1; i >= 0; --i) { + if (!fieldMaskContains(fieldMaskPaths, keys[i])) { + const path = keys[i]; + const value = values[i]; const childContext = context.childContextForFieldPath(path); if ( value instanceof SerializableFieldValue && @@ -424,130 +515,41 @@ export class UserDataReader { updateData.set(path, parsedValue); } } - }); - - const mask = new FieldMask(fieldMaskPaths); - return new ParsedUpdateData( - updateData.build(), - mask, - context.fieldTransforms - ); - } - - /** Parse update data from a list of field/value arguments. */ - parseUpdateVarargs( - methodName: string, - targetDoc: DocumentKey, - field: string | BaseFieldPath, - value: unknown, - moreFieldsAndValues: unknown[] - ): ParsedUpdateData { - const context = this.createContext( - UserDataSource.Update, - methodName, - targetDoc - ); - const keys = [fieldPathFromArgument(methodName, field, targetDoc)]; - const values = [value]; - - if (moreFieldsAndValues.length % 2 !== 0) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - `Function ${methodName}() needs to be called with an even number ` + - 'of arguments that alternate between field names and values.' - ); - } - - for (let i = 0; i < moreFieldsAndValues.length; i += 2) { - keys.push( - fieldPathFromArgument( - methodName, - moreFieldsAndValues[i] as string | BaseFieldPath - ) - ); - values.push(moreFieldsAndValues[i + 1]); - } - - const fieldMaskPaths: FieldPath[] = []; - const updateData = new ObjectValueBuilder(); - - // We iterate in reverse order to pick the last value for a field if the - // user specified the field multiple times. - for (let i = keys.length - 1; i >= 0; --i) { - if (!fieldMaskContains(fieldMaskPaths, keys[i])) { - const path = keys[i]; - const value = values[i]; - const childContext = context.childContextForFieldPath(path); - if ( - value instanceof SerializableFieldValue && - value._delegate instanceof DeleteFieldValueImpl - ) { - // Add it to the field mask, but don't add anything to updateData. - fieldMaskPaths.push(path); - } else { - const parsedValue = parseData(value, childContext); - if (parsedValue != null) { - fieldMaskPaths.push(path); - updateData.set(path, parsedValue); - } - } - } } - - const mask = new FieldMask(fieldMaskPaths); - return new ParsedUpdateData( - updateData.build(), - mask, - context.fieldTransforms - ); } - /** Creates a new top-level parse context. */ - private createContext( - dataSource: UserDataSource, - methodName: string, - targetDoc?: DocumentKey, - hasConverter = false - ): ParseContext { - return new ParseContext( - { - dataSource, - methodName, - targetDoc, - path: FieldPath.emptyPath(), - arrayElement: false, - hasConverter - }, - this.databaseId, - this.serializer, - this.ignoreUndefinedProperties - ); - } + const mask = new FieldMask(fieldMaskPaths); + return new ParsedUpdateData( + updateData.build(), + mask, + context.fieldTransforms + ); +} - /** - * Parse a "query value" (e.g. value in a where filter or a value in a cursor - * bound). - * - * @param allowArrays Whether the query value is an array that may directly - * contain additional arrays (e.g. the operand of an `in` query). - */ - parseQueryValue( - methodName: string, - input: unknown, - allowArrays = false - ): api.Value { - const context = this.createContext( - allowArrays ? UserDataSource.ArrayArgument : UserDataSource.Argument, - methodName - ); - const parsed = parseData(input, context); - debugAssert(parsed != null, 'Parsed data should not be null.'); - debugAssert( - context.fieldTransforms.length === 0, - 'Field transforms should have been disallowed.' - ); - return parsed; - } +/** + * Parse a "query value" (e.g. value in a where filter or a value in a cursor + * bound). + * + * @param allowArrays Whether the query value is an array that may directly + * contain additional arrays (e.g. the operand of an `in` query). + */ +export function parseQueryValue( + userDataReader: UserDataReader, + methodName: string, + input: unknown, + allowArrays = false +): api.Value { + const context = userDataReader.createContext( + allowArrays ? UserDataSource.ArrayArgument : UserDataSource.Argument, + methodName + ); + const parsed = parseData(input, context); + debugAssert(parsed != null, 'Parsed data should not be null.'); + debugAssert( + context.fieldTransforms.length === 0, + 'Field transforms should have been disallowed.' + ); + return parsed; } /** diff --git a/packages/firestore/test/unit/remote/serializer.helper.ts b/packages/firestore/test/unit/remote/serializer.helper.ts index 376f7ab0174..52c4362d461 100644 --- a/packages/firestore/test/unit/remote/serializer.helper.ts +++ b/packages/firestore/test/unit/remote/serializer.helper.ts @@ -28,10 +28,10 @@ import { ArrayContainsFilter, Direction, FieldFilter, + filterEquals, InFilter, KeyFieldFilter, Operator, - filterEquals, OrderBy, Query } from '../../../src/core/query'; @@ -109,6 +109,7 @@ import { } from '../../util/helpers'; import { ByteString } from '../../../src/util/byte_string'; +import { parseQueryValue } from '../../../src/api/user_data_reader'; const userDataWriter = testUserDataWriter(); const protobufJsonReader = testUserDataReader(/* useProto3Json= */ true); @@ -165,7 +166,8 @@ export function serializerTest( protoJsValue = protoJsValue ?? jsonValue; // Convert value to JSON and verify. - const actualJsonProto = protobufJsonReader.parseQueryValue( + const actualJsonProto = parseQueryValue( + protobufJsonReader, 'verifyFieldValueRoundTrip', value ); @@ -184,7 +186,8 @@ export function serializerTest( } // Convert value to ProtoJs and verify. - const actualProtoJsProto = protoJsReader.parseQueryValue( + const actualProtoJsProto = parseQueryValue( + protoJsReader, 'verifyFieldValueRoundTrip', value ); @@ -354,28 +357,32 @@ export function serializerTest( it('converts TimestampValue to string (useProto3Json=true)', () => { expect( - protobufJsonReader.parseQueryValue( + parseQueryValue( + protobufJsonReader, 'timestampConversion', new Timestamp(1488872578, 916123000) ) ).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.916123000Z' }); expect( - protobufJsonReader.parseQueryValue( + parseQueryValue( + protobufJsonReader, 'timestampConversion', new Timestamp(1488872578, 916000000) ) ).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.916000000Z' }); expect( - protobufJsonReader.parseQueryValue( + parseQueryValue( + protobufJsonReader, 'timestampConversion', new Timestamp(1488872578, 916000) ) ).to.deep.equal({ timestampValue: '2017-03-07T07:42:58.000916000Z' }); expect( - protobufJsonReader.parseQueryValue( + parseQueryValue( + protobufJsonReader, 'timestampConversion', new Timestamp(1488872578, 0) ) diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index 1aca63f36bd..641c4add760 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -24,7 +24,11 @@ import { expect } from 'chai'; import { Blob } from '../../src/api/blob'; import { fromDotSeparatedString } from '../../src/api/field_path'; import { UserDataWriter } from '../../src/api/user_data_writer'; -import { UserDataReader } from '../../src/api/user_data_reader'; +import { + parseQueryValue, + parseUpdateData, + UserDataReader +} from '../../src/api/user_data_reader'; import { DatabaseId } from '../../src/core/database_info'; import { Bound, @@ -163,7 +167,7 @@ export function wrap(value: unknown): api.Value { // HACK: We use parseQueryValue() since it accepts scalars as well as // arrays / objects, and our tests currently use wrap() pretty generically so // we don't know the intent. - return testUserDataReader().parseQueryValue('wrap', value); + return parseQueryValue(testUserDataReader(), 'wrap', value); } export function wrapObject(obj: JsonObject): ObjectValue { @@ -233,7 +237,8 @@ export function patchMutation( } }); const patchKey = key(keyStr); - const parsed = testUserDataReader().parseUpdateData( + const parsed = parseUpdateData( + testUserDataReader(), 'patchMutation', patchKey, json @@ -261,7 +266,8 @@ export function transformMutation( data: Dict ): TransformMutation { const transformKey = key(keyStr); - const result = testUserDataReader().parseUpdateData( + const result = parseUpdateData( + testUserDataReader(), 'transformMutation()', transformKey, data From 544fe754837af9b053530982907c206d82404887 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 1 Jul 2020 09:29:31 -0700 Subject: [PATCH 3/5] Create rotten-hats-fry.md --- .changeset/rotten-hats-fry.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/rotten-hats-fry.md diff --git a/.changeset/rotten-hats-fry.md b/.changeset/rotten-hats-fry.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/rotten-hats-fry.md @@ -0,0 +1,2 @@ +--- +--- From 3a82fcf5fa6abd3dacf247f58a7673efb2b11887 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Wed, 1 Jul 2020 09:58:07 -0700 Subject: [PATCH 4/5] Cleanup --- packages/firestore/src/model/mutation.ts | 8 ++++---- packages/firestore/src/model/transform_operation.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index bef77cfd819..7c585225d93 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -90,12 +90,12 @@ export class FieldTransform { } export function fieldTransformEquals( - l: FieldTransform, - r: FieldTransform + left: FieldTransform, + right: FieldTransform ): boolean { return ( - l.field.isEqual(r.field) && - transformOperationEquals(l.transform, r.transform) + left.field.isEqual(right.field) && + transformOperationEquals(left.transform, right.transform) ); } diff --git a/packages/firestore/src/model/transform_operation.ts b/packages/firestore/src/model/transform_operation.ts index 5c6fc64ee0b..86c69c7dde9 100644 --- a/packages/firestore/src/model/transform_operation.ts +++ b/packages/firestore/src/model/transform_operation.ts @@ -47,7 +47,7 @@ export function applyTransformOperationToLocalView( localWriteTime: Timestamp ): api.Value { if (transform instanceof ServerTimestampTransform) { - return serverTimestamp(localWriteTime!, previousValue); + return serverTimestamp(localWriteTime, previousValue); } else if (transform instanceof ArrayUnionTransformOperation) { return applyArrayUnionTransformOperation(transform, previousValue); } else if (transform instanceof ArrayRemoveTransformOperation) { From d0cb52b121a2c847da6f739829e5cbaa015896ea Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Mon, 6 Jul 2020 15:25:54 -0700 Subject: [PATCH 5/5] Create four-melons-laugh.md --- .changeset/four-melons-laugh.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/four-melons-laugh.md diff --git a/.changeset/four-melons-laugh.md b/.changeset/four-melons-laugh.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/four-melons-laugh.md @@ -0,0 +1,2 @@ +--- +---