From 39653d97dab39e00ccbef2820db2a591332474d3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 18 Jan 2024 14:58:15 +0100 Subject: [PATCH 1/2] fix(utils): Ensure `dropUndefinedKeys()` does not break class instances --- packages/utils/src/is.ts | 2 +- packages/utils/src/object.ts | 15 ++++++++++++++- packages/utils/test/object.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 12225b9c8b60..1e84e1873006 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -106,7 +106,7 @@ export function isPrimitive(wat: unknown): wat is Primitive { } /** - * Checks whether given value's type is an object literal + * Checks whether given value's type is an object literal, or a class instance. * {@link isPlainObject}. * * @param wat A value to be checked. diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index 6dbe15488325..a5300bf586fd 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -222,7 +222,7 @@ export function dropUndefinedKeys(inputValue: T): T { } function _dropUndefinedKeys(inputValue: T, memoizationMap: Map): T { - if (isPlainObject(inputValue)) { + if (isPojo(inputValue)) { // If this node has already been visited due to a circular reference, return the object it was mapped to in the new object const memoVal = memoizationMap.get(inputValue); if (memoVal !== undefined) { @@ -263,6 +263,19 @@ function _dropUndefinedKeys(inputValue: T, memoizationMap: Map { + if (!isPlainObject(input)) { + return false; + } + + try { + const name = (Object.getPrototypeOf(input) as { constructor: { name: string } }).constructor.name; + return !name || name === 'Object'; + } catch { + return true; + } +} + /** * Ensure that something is an object. * diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts index 5deac39e8cbc..6fbb69f0e2b4 100644 --- a/packages/utils/test/object.test.ts +++ b/packages/utils/test/object.test.ts @@ -210,6 +210,29 @@ describe('dropUndefinedKeys()', () => { }); }); + describe('class instances', () => { + class MyClass { + public a = 'foo'; + public b = undefined; + } + + test('ignores class instance', () => { + const instance = new MyClass(); + const result = dropUndefinedKeys(instance); + expect(result).toEqual({ a: 'foo', b: undefined }); + expect(result).toBeInstanceOf(MyClass); + expect(Object.prototype.hasOwnProperty.call(result, 'b')).toBe(true); + }); + + test('ignores nested instances', () => { + const instance = new MyClass(); + const result = dropUndefinedKeys({ a: [instance] }); + expect(result).toEqual({ a: [instance] }); + expect(result.a[0]).toBeInstanceOf(MyClass); + expect(Object.prototype.hasOwnProperty.call(result.a[0], 'b')).toBe(true); + }); + }); + test('should not throw on objects with circular reference', () => { const chicken: any = { food: undefined, From ae34000391be5c65e28d973a1c5e31a9c62c1b92 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 18 Jan 2024 15:18:20 +0100 Subject: [PATCH 2/2] add test --- packages/utils/test/is.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index da9d77a44fde..c97c751cb6d3 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -5,6 +5,7 @@ import { isErrorEvent, isInstanceOf, isNaN, + isPlainObject, isPrimitive, isThenable, isVueViewModel, @@ -144,3 +145,27 @@ describe('isVueViewModel()', () => { expect(isVueViewModel({ foo: true })).toEqual(false); }); }); + +describe('isPlainObject', () => { + class MyClass { + public foo: string = 'bar'; + } + + it.each([ + [{}, true], + [true, false], + [false, false], + [undefined, false], + [null, false], + ['', false], + [1, false], + [0, false], + [{ aha: 'yes' }, true], + [new Object({ aha: 'yes' }), true], + [new String('aa'), false], + [new MyClass(), true], + [{ ...new MyClass() }, true], + ])('%s is %s', (value, expected) => { + expect(isPlainObject(value)).toBe(expected); + }); +});