From 29c3e9b6881030b6fe3b75a35ca536d009cd2d3a Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Thu, 3 Oct 2019 10:41:12 -0700 Subject: [PATCH] fix: provide custom error for FieldValue subclasses (#771) This PR extends the custom errors for instances of FieldValue classes that do not match the expected prototype to the FieldValue sentinel classes FieldTransform, ServerTimestampTransform, ArrayUnionTransform, ArrayRemoveTransform and NumericIncrementTransform. Fixes https://github.com/googleapis/nodejs-firestore/issues/760 --- dev/src/validate.ts | 21 ++++++++++++++++++++- dev/test/document.ts | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/dev/src/validate.ts b/dev/src/validate.ts index bcbd2b718..c7378cacd 100644 --- a/dev/src/validate.ts +++ b/dev/src/validate.ts @@ -37,6 +37,22 @@ export interface NumericRangeOptions { maxValue?: number; } +/** + * Returns the name of the base class (ignoring Object). + * + * @private + * @param value The object whose base class name to extract. + * @returns The name of the base class constructor or "Object" if value does not + * extend a custom class. + */ +function extractBaseClassName(value: object): string { + let constructorName = 'Object'; + while (Object.getPrototypeOf(value) !== Object.prototype) { + value = Object.getPrototypeOf(value); + constructorName = value.constructor.name; + } + return constructorName; +} /** * Generates an error message to use with custom objects that cannot be * serialized. @@ -54,7 +70,10 @@ export function customObjectMessage( const fieldPathMessage = path ? ` (found in field ${path})` : ''; if (isObject(value)) { - const typeName = value.constructor.name; + // We use the base class name as the type name as the sentinel classes + // returned by the public FieldValue API are subclasses of FieldValue. By + // using the base name, we reduce the number of special cases below. + const typeName = extractBaseClassName(value); switch (typeName) { case 'DocumentReference': case 'FieldPath': diff --git a/dev/test/document.ts b/dev/test/document.ts index 7648b2683..39840984e 100644 --- a/dev/test/document.ts +++ b/dev/test/document.ts @@ -170,6 +170,29 @@ describe('serialize document', () => { ); }); + it('provides custom error for objects from different Firestore instance', () => { + class FieldPath {} + class CustomFieldPath extends FieldPath {} + class VeryCustomFieldPath extends CustomFieldPath {} + + const customClasses = [ + new FieldPath(), + new CustomFieldPath(), + new VeryCustomFieldPath(), + ]; + + for (const customClass of customClasses) { + expect(() => { + firestore + .doc('collectionId/documentId') + .set(customClass as InvalidApiUsage); + }).to.throw( + 'Value for argument "data" is not a valid Firestore document. ' + + 'Detected an object of type "FieldPath" that doesn\'t match the expected instance.' + ); + } + }); + it('serializes large numbers into doubles', () => { const overrides: ApiOverride = { commit: (request, options, callback) => {