From c357cc32e462a86889e3619e133e3d74138bacb6 Mon Sep 17 00:00:00 2001 From: Jannis Leifeld Date: Mon, 5 Feb 2024 09:04:56 +0100 Subject: [PATCH] NEXT-32745 - fix-circular-loop-in-serializer --- package-lock.json | 15 ++- package.json | 1 + .../serializer/criteria-serializer.ts | 4 +- .../entity-collection-serializer.ts | 4 +- .../serializer/entity-serializer.spec.ts | 119 ++++++++++++++++++ .../serializer/entity-serializer.ts | 6 +- src/_internals/serializer/index.ts | 26 +++- 7 files changed, 164 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4809a28..e6e45ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@shopware-ag/meteor-admin-sdk", - "version": "3.0.16", + "version": "3.0.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shopware-ag/meteor-admin-sdk", - "version": "3.0.16", + "version": "3.0.17", "license": "MIT", "dependencies": { + "json-complete": "^2.0.1", "localforage": "^1.10.0", "lodash": "^4.17.21" }, @@ -8430,6 +8431,11 @@ "node": ">=4" } }, + "node_modules/json-complete": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/json-complete/-/json-complete-2.0.1.tgz", + "integrity": "sha512-i2cSMhWdGm42tfkvfY9Z4h99I6xKnWK0lfvZghmB9O7C2cAHIaV2V30RT7ZwOaPc8w9Y2GLVtlDrOVjjk/DdPg==" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -17671,6 +17677,11 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-complete": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/json-complete/-/json-complete-2.0.1.tgz", + "integrity": "sha512-i2cSMhWdGm42tfkvfY9Z4h99I6xKnWK0lfvZghmB9O7C2cAHIaV2V30RT7ZwOaPc8w9Y2GLVtlDrOVjjk/DdPg==" + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/package.json b/package.json index 040f253..8ab18be 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "wait-on": "^6.0.1" }, "dependencies": { + "json-complete": "^2.0.1", "localforage": "^1.10.0", "lodash": "^4.17.21" } diff --git a/src/_internals/serializer/criteria-serializer.ts b/src/_internals/serializer/criteria-serializer.ts index 27a11ab..2b11725 100644 --- a/src/_internals/serializer/criteria-serializer.ts +++ b/src/_internals/serializer/criteria-serializer.ts @@ -6,11 +6,11 @@ import type { SerializerFactory } from './index'; const CriteriaSerializer: SerializerFactory = () => ({ name: 'criteria', - serialize: ({ value, customizerMethod }): any => { + serialize: ({ value, customizerMethod, seen }): any => { if (value instanceof Criteria) { return { __type__: '__Criteria__', - data: customizerMethod(value.getCriteriaData()), + data: customizerMethod(value.getCriteriaData(), seen), }; } }, diff --git a/src/_internals/serializer/entity-collection-serializer.ts b/src/_internals/serializer/entity-collection-serializer.ts index 0abfd80..512d324 100644 --- a/src/_internals/serializer/entity-collection-serializer.ts +++ b/src/_internals/serializer/entity-collection-serializer.ts @@ -6,7 +6,7 @@ import type { SerializerFactory } from '.'; const EntityCollectionSerializerFactory: SerializerFactory = () => ({ name: 'entity-collection', - serialize: ({ value, customizerMethod }): any => { + serialize: ({ value, customizerMethod, seen }): any => { if (value instanceof EntityCollection || (value?.__identifier__ && value.__identifier__() === 'EntityCollection')) { return customizerMethod({ __type__: '__EntityCollection__', @@ -17,7 +17,7 @@ const EntityCollectionSerializerFactory: SerializerFactory = () => ({ __entities__: Array.from(value), __total__: value.total, __aggregations__: value.aggregations, - }); + }, seen); } }, diff --git a/src/_internals/serializer/entity-serializer.spec.ts b/src/_internals/serializer/entity-serializer.spec.ts index 495d143..4b34af3 100644 --- a/src/_internals/serializer/entity-serializer.spec.ts +++ b/src/_internals/serializer/entity-serializer.spec.ts @@ -160,4 +160,123 @@ describe('entity-serializer.ts', () => { expect(entityKeys).not.toContain('_isDirty'); expect(entityKeys).not.toContain('_isNew'); }); + + it('should convert entities with circular reference', () => { + jest.setTimeout(5000); + + const originValues = { + string: 'jest-is-fun', + number: 42, + array: ['jest', 4, 'you'], + object: { + foo: 'bar', + }, + lineItems: [ + { + name: 'Item1', + price: 10, + children: [ + { + name: 'Item1.1', + price: 2.5, + parent: undefined, + }, + ], + }, + ], + }; + + // Introduce circular reference + // @ts-expect-error + originValues.lineItems[0].children[0].parent = originValues.lineItems[0]; + + // @ts-expect-error - we know that this entity does not exist + const entity = new Entity('foo', 'jest', { + ...cloneDeep(originValues), + }) as any; + + entity.string = 'jest-is-more-fun'; + entity.number = 1337; + entity.array.push('and me'); + entity.object.foo = 'buz'; + entity.parent = entity; + + const messageData = { + entity, + }; + + // Check if private properties are hidden behind the proxy + const entityKeysBefore = Object.keys(messageData.entity); + expect(entityKeysBefore).not.toContain('_origin'); + expect(entityKeysBefore).not.toContain('_isDirty'); + expect(entityKeysBefore).not.toContain('_isNew'); + + const serializedMessageData = serialize(messageData); + + expect(serializedMessageData.entity.hasOwnProperty('__id__')).toBe(true); + expect(serializedMessageData.entity.__id__).toBe('foo'); + + expect(serializedMessageData.entity.hasOwnProperty('__entityName__')).toBe(true); + expect(serializedMessageData.entity.__entityName__).toBe('jest'); + + expect(serializedMessageData.entity.hasOwnProperty('__isDirty__')).toBe(true); + expect(serializedMessageData.entity.__isDirty__).toBe(true); + + expect(serializedMessageData.entity.hasOwnProperty('__isNew__')).toBe(true); + expect(serializedMessageData.entity.__isNew__).toBe(false); + + expect(serializedMessageData.entity.hasOwnProperty('__origin__')).toBe(true); + expect(typeof serializedMessageData.entity.__origin__).toBe('object'); + + expect(serializedMessageData.entity.__origin__.hasOwnProperty('string')).toBe(true); + expect(serializedMessageData.entity.__origin__.string).toBe('jest-is-fun'); + + expect(serializedMessageData.entity.__origin__.hasOwnProperty('number')).toBe(true); + expect(serializedMessageData.entity.__origin__.number).toBe(42); + + expect(serializedMessageData.entity.__origin__.hasOwnProperty('array')).toBe(true); + expect(serializedMessageData.entity.__origin__.array.length).toBe(3); + + expect(serializedMessageData.entity.__origin__.hasOwnProperty('object')).toBe(true); + expect(typeof serializedMessageData.entity.__origin__.object).toBe('object'); + expect(serializedMessageData.entity.__origin__.object.hasOwnProperty('foo')).toBe(true); + expect(serializedMessageData.entity.__origin__.object.foo).toBe('bar'); + + expect(serializedMessageData.entity.hasOwnProperty('__draft__')).toBe(true); + expect(typeof serializedMessageData.entity.__draft__).toBe('object'); + + expect(serializedMessageData.entity.__draft__.hasOwnProperty('string')).toBe(true); + expect(serializedMessageData.entity.__draft__.string).toBe('jest-is-more-fun'); + + expect(serializedMessageData.entity.__draft__.hasOwnProperty('number')).toBe(true); + expect(serializedMessageData.entity.__draft__.number).toBe(1337); + + expect(serializedMessageData.entity.__draft__.hasOwnProperty('array')).toBe(true); + expect(serializedMessageData.entity.__draft__.array.length).toBe(4); + + expect(serializedMessageData.entity.__draft__.hasOwnProperty('object')).toBe(true); + expect(typeof serializedMessageData.entity.__draft__.object).toBe('object'); + expect(serializedMessageData.entity.__draft__.object.hasOwnProperty('foo')).toBe(true); + expect(serializedMessageData.entity.__draft__.object.foo).toBe('buz'); + + const deserializedMessageData = deserialize(serializedMessageData, new MessageEvent('')); + + // Assert entity values + expect(deserializedMessageData.entity.getIsDirty()).toBe(true); + expect(deserializedMessageData.entity.isNew()).toBe(false); + expect(deserializedMessageData.entity.string).toBe('jest-is-more-fun'); + expect(deserializedMessageData.entity.number).toBe(1337); + expect(deserializedMessageData.entity.array.length).toBe(4); + expect(deserializedMessageData.entity.array[0]).toBe('jest'); + expect(deserializedMessageData.entity.array[1]).toBe(4); + expect(deserializedMessageData.entity.array[2]).toBe('you'); + expect(deserializedMessageData.entity.array[3]).toBe('and me'); + expect(deserializedMessageData.entity.object.foo).toBe('buz'); + + // Check if private properties are hidden behind the proxy + const entityKeys = Object.keys(deserializedMessageData.entity); + expect(entityKeys).not.toContain('_origin'); + expect(entityKeys).not.toContain('_isDirty'); + expect(entityKeys).not.toContain('_isNew'); + }); }); diff --git a/src/_internals/serializer/entity-serializer.ts b/src/_internals/serializer/entity-serializer.ts index 31a8dcf..57c2394 100644 --- a/src/_internals/serializer/entity-serializer.ts +++ b/src/_internals/serializer/entity-serializer.ts @@ -6,7 +6,7 @@ import type { SerializerFactory } from '.'; const EntitySerializerFactory: SerializerFactory = () => ({ name: 'entity', - serialize: ({ value, customizerMethod }): any => { + serialize: ({ value, customizerMethod, seen }): any => { if (!isObject(value) || typeof value.__identifier__ !== 'function' || value.__identifier__() !== 'Entity') { return; } @@ -17,8 +17,8 @@ const EntitySerializerFactory: SerializerFactory = () => ({ __entityName__: value._entityName, __isDirty__: value._isDirty, __isNew__: value._isNew, - __origin__: customizerMethod(value._origin), - __draft__: customizerMethod(value._draft), + __origin__: customizerMethod(value._origin, seen), + __draft__: customizerMethod(value._draft, seen), }; }, diff --git a/src/_internals/serializer/index.ts b/src/_internals/serializer/index.ts index f689537..27f8ac2 100644 --- a/src/_internals/serializer/index.ts +++ b/src/_internals/serializer/index.ts @@ -19,12 +19,21 @@ interface customizerProperties { object: any | undefined, stack: any, event?: MessageEvent, + customizerMethod: (messageData: any, seen: Map, event?: MessageEvent) => any, + seen: Map, +} + +interface deserializeCustomizerProperties extends Omit< + customizerProperties, + 'customizerMethod' | 'seen' +> { customizerMethod: (messageData: any, event?: MessageEvent) => any, } + interface serializer { name: string, serialize: (customizerProperties: customizerProperties) => any, - deserialize: (customizerProperties: customizerProperties) => any, + deserialize: (customizerProperties: deserializeCustomizerProperties) => any, } export type SerializerFactory = (dependencies: SerializerDependencies) => serializer; @@ -63,8 +72,18 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci } /* eslint-disable */ - function serialize(messageData: any): any { + function serialize(messageData: any, seen = new Map()): any { return cloneDeepWith(messageData, (value, key, object, stack) => { + if (seen.has(value)) { + if (typeof seen.get(value) === 'string' && seen.get(value).startsWith('$#')) { + return; + } + + return seen.get(value); + } + + seen.set(value, `$#${Math.random()}`); + // return first matching serializer result for (const serializer of serializers) { const result = serializer.serialize({ @@ -73,9 +92,12 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci object, stack, customizerMethod: serialize, + seen, }); if (result) { + // console.log('object', object) + seen.set(value, result) return result; }; }