Skip to content
This repository has been archived by the owner on Mar 28, 2024. It is now read-only.

Commit

Permalink
NEXT-32745 - Fix circular references
Browse files Browse the repository at this point in the history
  • Loading branch information
seggewiss committed Feb 6, 2024
1 parent e3efad8 commit 20d7ad3
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 52 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to this project will be documented in this file.

## [3.1.0] - 06.02.2024

## Fixed
- Fixed an issue with circular references in json structures causing pages to freeze

## [3.0.17] - 19.01.2024

## Added
Expand Down
15 changes: 2 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@shopware-ag/meteor-admin-sdk",
"license": "MIT",
"version": "3.0.17",
"version": "3.1.0",
"repository": {
"type": "git",
"url": "git://github.com/shopware/meteor-admin-sdk.git"
Expand Down Expand Up @@ -110,7 +110,6 @@
"wait-on": "^6.0.1"
},
"dependencies": {
"json-complete": "^2.0.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21"
}
Expand Down
4 changes: 2 additions & 2 deletions src/_internals/serializer/criteria-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type { SerializerFactory } from './index';
const CriteriaSerializer: SerializerFactory = () => ({
name: 'criteria',

serialize: ({ value, customizerMethod, seen }): any => {
serialize: ({ value, customizerMethod, seen, path }): any => {
if (value instanceof Criteria) {
return {
__type__: '__Criteria__',
data: customizerMethod(value.getCriteriaData(), seen),
data: customizerMethod(value.getCriteriaData(), seen, path),
};
}
},
Expand Down
26 changes: 15 additions & 11 deletions src/_internals/serializer/entity-collection-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ import type { SerializerFactory } from '.';
const EntityCollectionSerializerFactory: SerializerFactory = () => ({
name: 'entity-collection',

serialize: ({ value, customizerMethod, seen }): any => {
serialize: ({ value, customizerMethod, seen, path }): any => {
if (value instanceof EntityCollection || (value?.__identifier__ && value.__identifier__() === 'EntityCollection')) {
return customizerMethod({
__type__: '__EntityCollection__',
__source__: value.source,
__entityName__: value.entity,
__context__: value.context,
__criteria__: value.criteria,
__entities__: Array.from(value),
__total__: value.total,
__aggregations__: value.aggregations,
}, seen);
return customizerMethod(
{
__type__: '__EntityCollection__',
__source__: value.source,
__entityName__: value.entity,
__context__: value.context,
__criteria__: value.criteria,
__entities__: Array.from(value),
__total__: value.total,
__aggregations__: value.aggregations,
},
seen,
path
);
}
},

Expand Down
6 changes: 3 additions & 3 deletions src/_internals/serializer/entity-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { SerializerFactory } from '.';
const EntitySerializerFactory: SerializerFactory = () => ({
name: 'entity',

serialize: ({ value, customizerMethod, seen }): any => {
serialize: ({ value, customizerMethod, seen, path }): any => {
if (!isObject(value) || typeof value.__identifier__ !== 'function' || value.__identifier__() !== 'Entity') {
return;
}
Expand All @@ -17,8 +17,8 @@ const EntitySerializerFactory: SerializerFactory = () => ({
__entityName__: value._entityName,
__isDirty__: value._isDirty,
__isNew__: value._isNew,
__origin__: customizerMethod(value._origin, seen),
__draft__: customizerMethod(value._draft, seen),
__origin__: customizerMethod(value._origin, seen, path),
__draft__: customizerMethod(value._draft, seen, path),
};
},

Expand Down
78 changes: 78 additions & 0 deletions src/_internals/serializer/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Entity from '../../data/_internals/Entity';
import SerializerFactory from './index';
import { handle, send } from '../../channel';
import EntityCollection from '../../data/_internals/EntityCollection';
import Criteria from '../../data/Criteria';

const { serialize, deserialize } = SerializerFactory({
handle: handle,
send: send,
})

describe('index.ts', () => {
it('should convert entities with circular reference', () => {
// @ts-ignore
const entity = new Entity('foo', 'jest', {
// @ts-ignore
lineItems: new EntityCollection('line_items', 'line_item', undefined, new Criteria(), [
// @ts-ignore
new Entity('line_item_1', 'line_item', {
id: 'line_item_1',
quantity: 1,
versionId: 1,
price: {
unitPrice: 100,
totalPrice: 100,
},
// @ts-ignore
children: new EntityCollection('line_item', 'order_line_item', undefined, new Criteria(), [
// @ts-ignore
new Entity('extension', 'order_line_item_foreign_keys_extension', {
id: 'extension',
key: 'value',
parent: undefined,
}),
// @ts-ignore
new Entity('extension', 'order_line_item_foreign_keys_extension', {
id: 'extension2',
key: 'value',
})
]),
}),
// @ts-ignore
new Entity('line_item_2', 'line_item', {
id: 'line_item_2',
quantity: 1,
versionId: 1,
price: {
unitPrice: 100,
totalPrice: 100,
},
}),
]),
});

// @ts-ignore - Create circular reference
entity.lineItems.first(0).children.getAt(0).parent = entity.lineItems.getAt(0);

const messageData = {
entity,
};

const result = serialize(messageData);

// this is a regression test for a bug that caused the serializer to fail
// expect the circular reference to be converted
expect(result.entity.__draft__.lineItems.__entities__[0].__draft__.children.__entities__[0] instanceof Entity).toBe(false);
expect(result.entity.__draft__.lineItems.__entities__[0].__draft__.children.__entities__[1] instanceof Entity).toBe(false);
expect(result.entity.__draft__.lineItems.__entities__[0].__draft__.children.__entities__[0].__draft__.parent.children instanceof EntityCollection).toBe(false);
expect(result.entity.__draft__.lineItems.__entities__[0].__draft__.children.__entities__[0].__draft__.parent).toStrictEqual({
__$CR__: 'root.entity.lineItems.0'
});

const deserialized = deserialize(result, new MessageEvent('message'));

// expect the circular reference to be converted back
expect(deserialized.entity.lineItems.getAt(0).children.getAt(0).parent).toBe(deserialized.entity.lineItems.getAt(0));
});
});
59 changes: 46 additions & 13 deletions src/_internals/serializer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import CriteriaSerializer from './criteria-serializer';
import EntitySerializer from './entity-serializer';
import EntityCollectionSerializer from './entity-collection-serializer';
import HandleErrorSerializer from './handle-error-serializer';
import cloneDeepWith from 'lodash/cloneDeepWith';
import { cloneDeepWith, get, set } from 'lodash';
import MissingPrivilegesErrorSerializer from './missing-priviliges-error-serializer';
import { isPrimitive, traverseObject, removeRoot } from '../utils';

interface SerializerDependencies {
send: typeof send,
Expand All @@ -19,13 +20,14 @@ interface customizerProperties {
object: any | undefined,
stack: any,
event?: MessageEvent<string>,
customizerMethod: (messageData: any, seen: Map<any, any>, event?: MessageEvent<string>) => any,
customizerMethod: (messageData: any, seen: Map<any, any>, path: string, event?: MessageEvent<string>) => any,
seen: Map<any, any>,
path: string,
}

interface deserializeCustomizerProperties extends Omit<
customizerProperties,
'customizerMethod' | 'seen'
'customizerMethod' | 'seen' | 'path'
> {
customizerMethod: (messageData: any, event?: MessageEvent<string>) => any,
}
Expand Down Expand Up @@ -72,17 +74,26 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci
}

/* eslint-disable */
function serialize(messageData: any, seen = new Map()): any {
function serialize(messageData: any, seen = new Map(), path = 'root'): any {
return cloneDeepWith<unknown>(messageData, (value, key, object, stack) => {
if (seen.has(value)) {
if (typeof seen.get(value) === 'string' && seen.get(value).startsWith('$#')) {
return;
}
// track the path to the current value
let p = path + '.' + key;

// early return for primitives to save some computation
if (isPrimitive(value)) {
return value;
}

return seen.get(value);
// encountered this value before === circular reference
if (seen.has(value)) {
// replace the circular reference with a reference object containing its origin
return {
__$CR__: seen.get(value),
};
}

seen.set(value, `$#${Math.random()}`);
// save the path to the current value
seen.set(value, p);

// return first matching serializer result
for (const serializer of serializers) {
Expand All @@ -93,11 +104,10 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci
stack,
customizerMethod: serialize,
seen,
path: p,
});

if (result) {
// console.log('object', object)
seen.set(value, result)
return result;
};
}
Expand All @@ -106,6 +116,29 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci


function deserialize(messageData: any, event?: MessageEvent<string>): any {
// restore all entities, collections and other serialized objects
const desirialized = _deserialize(messageData, event);

// restore circular references
traverseObject(desirialized, (_, key, value, previousKey) => {
// check if the current key is a circular reference identifier
if (key !== '__$CR__') {
return;
}

// the path to the value going to be restored as a circular reference
const path = removeRoot(value);
// the path where the circular reference should be restored
const reference = removeRoot(previousKey);

// restore the circular reference
set(desirialized, reference, get(desirialized, path));
});

return desirialized;
}

function _deserialize(messageData: any, event?: MessageEvent<string>): any {
return cloneDeepWith<unknown>(messageData, (value, key, object, stack) => {
// return first matching serializer result
for (const serializer of serializers) {
Expand All @@ -115,7 +148,7 @@ export default function mainSerializerFactory(dependencies: SerializerDependenci
object,
stack,
event,
customizerMethod: deserialize,
customizerMethod: _deserialize,
});

if (result) {
Expand Down
27 changes: 19 additions & 8 deletions src/_internals/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,30 @@ export function hasOwnProperty(obj: any, path: string): boolean {
}


export function traverseObject(this: any, traversableObject: any, processor: (parentEntry: any, key: string, value: any) => void, seen: Map<any, any> = new Map()) {
export function traverseObject(this: any, traversableObject: any, processor: (parentEntry: any, key: string, value: any, previousKey: string) => void, previousKey = 'root') {
for (let index in traversableObject) {
const currentEntry = traversableObject[index];
if (seen.has(currentEntry)) {
continue;
}

seen.set(currentEntry, true);

processor.apply(this, [traversableObject, index, currentEntry]);
processor.apply(this, [traversableObject, index, currentEntry, previousKey]);

if (isObject(currentEntry)) {
traverseObject(currentEntry, processor, seen);
let pk = previousKey + '.' + index;
traverseObject(currentEntry, processor, pk);
}
}
}

export function isPrimitive(value: any): boolean {
return value !== Object(value) || value === null || value === undefined;
}

/**
* Remove the root prefix from a path
*/
export function removeRoot(path: string | number): string | number {
if (typeof path !== 'string') {
return path;
}

return path.replace(/^root\./, '');
}

0 comments on commit 20d7ad3

Please sign in to comment.