fix(core): Remove circular refs from Code and push msg (#5741)

* remove circular refs from code items (and lint fixes)

* cleanup

---------

* add some tests

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Michael Auerswald 2023-03-21 15:34:30 +01:00 committed by GitHub
parent 199a91b398
commit b6d8a0f985
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 12 deletions

View file

@ -1,4 +1,4 @@
import { LoggerProxy as Logger } from 'n8n-workflow'; import { jsonStringify, LoggerProxy as Logger } from 'n8n-workflow';
import type { IPushDataType } from '@/Interfaces'; import type { IPushDataType } from '@/Interfaces';
import { eventBus } from '../eventbus'; import { eventBus } from '../eventbus';
@ -38,7 +38,7 @@ export abstract class AbstractPush<T> {
Logger.debug(`Send data of type "${type}" to editor-UI`, { dataType: type, sessionId }); Logger.debug(`Send data of type "${type}" to editor-UI`, { dataType: type, sessionId });
const sendData = JSON.stringify({ type, data }); const sendData = jsonStringify({ type, data }, { replaceCircularRefs: true });
if (sessionId === undefined) { if (sessionId === undefined) {
// Send to all connected clients // Send to all connected clients

View file

@ -135,7 +135,6 @@ export class SamlController {
private async handleInitSSO(res: express.Response) { private async handleInitSSO(res: express.Response) {
const result = this.samlService.getLoginRequestUrl(); const result = this.samlService.getLoginRequestUrl();
if (result?.binding === 'redirect') { if (result?.binding === 'redirect') {
// Return the redirect URL directly
return res.send(result.context.context); return res.send(result.context.context);
} else if (result?.binding === 'post') { } else if (result?.binding === 'post') {
return res.send(getInitSSOFormView(result.context as PostBindingContext)); return res.send(getInitSSOFormView(result.context as PostBindingContext));

View file

@ -12,15 +12,28 @@ function isTraversable(maybe: unknown): maybe is IDataObject {
* Stringify any non-standard JS objects (e.g. `Date`, `RegExp`) inside output items at any depth. * Stringify any non-standard JS objects (e.g. `Date`, `RegExp`) inside output items at any depth.
*/ */
export function standardizeOutput(output: IDataObject) { export function standardizeOutput(output: IDataObject) {
for (const [key, value] of Object.entries(output)) { const knownObjects = new WeakSet();
function standardizeOutputRecursive(obj: IDataObject): IDataObject {
for (const [key, value] of Object.entries(obj)) {
if (!isTraversable(value)) continue; if (!isTraversable(value)) continue;
output[key] = if (typeof value === 'object' && value !== null) {
value.constructor.name !== 'Object' if (knownObjects.has(value)) {
? JSON.stringify(value) // Date, RegExp, etc. // Found circular reference
: standardizeOutput(value); continue;
}
knownObjects.add(value);
} }
obj[key] =
value.constructor.name !== 'Object'
? JSON.stringify(value) // Date, RegExp, etc.
: standardizeOutputRecursive(value);
}
return obj;
}
standardizeOutputRecursive(output);
return output; return output;
} }

View file

@ -23,7 +23,7 @@ export * from './WorkflowErrors';
export * from './WorkflowHooks'; export * from './WorkflowHooks';
export * from './VersionedNodeType'; export * from './VersionedNodeType';
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
export { deepCopy, jsonParse, sleep, fileTypeFromMimeType, assert } from './utils'; export { deepCopy, jsonParse, jsonStringify, sleep, fileTypeFromMimeType, assert } from './utils';
export { export {
isINodeProperties, isINodeProperties,
isINodePropertyOptions, isINodePropertyOptions,

View file

@ -62,6 +62,31 @@ export const jsonParse = <T>(jsonString: string, options?: JSONParseOptions<T>):
} }
}; };
type JSONStringifyOptions = {
replaceCircularRefs?: boolean;
circularRefReplacement?: string;
};
const getReplaceCircularReferencesFn = (options: JSONStringifyOptions) => {
const knownObjects = new WeakSet();
return (key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (knownObjects.has(value)) {
return options?.circularRefReplacement ?? '[Circular Reference]';
}
knownObjects.add(value);
}
return value;
};
};
export const jsonStringify = (obj: unknown, options: JSONStringifyOptions = {}): string => {
const replacer = options?.replaceCircularRefs
? getReplaceCircularReferencesFn(options)
: undefined;
return JSON.stringify(obj, replacer);
};
export const sleep = async (ms: number): Promise<void> => export const sleep = async (ms: number): Promise<void> =>
new Promise((resolve) => { new Promise((resolve) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);

View file

@ -1,4 +1,4 @@
import { jsonParse, deepCopy } from '@/utils'; import { jsonParse, jsonStringify, deepCopy } from '@/utils';
describe('jsonParse', () => { describe('jsonParse', () => {
it('parses JSON', () => { it('parses JSON', () => {
@ -17,6 +17,21 @@ describe('jsonParse', () => {
}); });
}); });
describe('jsonStringify', () => {
const source: any = { a: 1, b: 2 };
source.c = source;
it('should throw errors on circular references by default', () => {
expect(() => jsonStringify(source)).toThrow('Converting circular structure to JSON');
});
it('should break circular references when requested', () => {
expect(jsonStringify(source, { replaceCircularRefs: true })).toEqual(
'{"a":1,"b":2,"c":"[Circular Reference]"}',
);
});
});
describe('deepCopy', () => { describe('deepCopy', () => {
it('should deep copy an object', () => { it('should deep copy an object', () => {
const serializable = { const serializable = {