perf: update deepCopy (#4364)

* perf: update deepCopy

* fix: using deepCopy in core and cli packages

* fix: using deepCopy in editor

* chore: formatting

* fix: some micro optimisation in deepCopy
This commit is contained in:
Csaba Tuncsik 2022-10-18 13:33:31 +02:00 committed by GitHub
parent 638d6f60d3
commit 1aa21ed3df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 101 additions and 27 deletions

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-underscore-dangle */
import { ICredentialDataDecryptedObject } from 'n8n-workflow'; import { deepCopy, ICredentialDataDecryptedObject } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { CredentialTypes, GenericHelpers, ICredentialsOverwrite } from '.'; import { CredentialTypes, GenericHelpers, ICredentialsOverwrite } from '.';
@ -19,7 +19,7 @@ class CredentialsOverwritesClass {
if (overwriteData !== undefined) { if (overwriteData !== undefined) {
// If data is already given it can directly be set instead of // If data is already given it can directly be set instead of
// loaded from environment // loaded from environment
this.__setData(JSON.parse(JSON.stringify(overwriteData))); this.__setData(deepCopy(overwriteData));
return; return;
} }
@ -57,14 +57,11 @@ class CredentialsOverwritesClass {
return data; return data;
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const returnData = deepCopy(data);
const returnData = JSON.parse(JSON.stringify(data));
// Overwrite only if there is currently no data set // Overwrite only if there is currently no data set
// eslint-disable-next-line no-restricted-syntax
for (const key of Object.keys(overwrites)) { for (const key of Object.keys(overwrites)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // @ts-ignore
if ([null, undefined, ''].includes(returnData[key])) { if ([null, undefined, ''].includes(returnData[key])) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
returnData[key] = overwrites[key]; returnData[key] = overwrites[key];
} }
} }

View file

@ -63,6 +63,7 @@ import {
NodeParameterValueType, NodeParameterValueType,
NodeExecutionWithMetadata, NodeExecutionWithMetadata,
IPairedItemData, IPairedItemData,
deepCopy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Agent } from 'https'; import { Agent } from 'https';
@ -1641,7 +1642,7 @@ export async function getCredentials(
* *
*/ */
export function getNode(node: INode): INode { export function getNode(node: INode): INode {
return JSON.parse(JSON.stringify(node)); return deepCopy(node);
} }
/** /**

View file

@ -16,6 +16,7 @@ import {
USER_SETTINGS_FILE_NAME, USER_SETTINGS_FILE_NAME,
USER_SETTINGS_SUBFOLDER, USER_SETTINGS_SUBFOLDER,
} from '.'; } from '.';
import { deepCopy } from 'n8n-workflow';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const { promisify } = require('util'); const { promisify } = require('util');
@ -173,7 +174,7 @@ export async function writeUserSettings(
} }
await fsWriteFile(settingsPath, JSON.stringify(settingsToWrite, null, '\t')); await fsWriteFile(settingsPath, JSON.stringify(settingsToWrite, null, '\t'));
settingsCache = JSON.parse(JSON.stringify(userSettings)); settingsCache = deepCopy(userSettings);
return userSettings; return userSettings;
} }

View file

@ -38,6 +38,7 @@ import {
} from '@/Interface'; } from '@/Interface';
import { import {
deepCopy,
INodeProperties, INodeProperties,
INodePropertyOptions, INodePropertyOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -161,7 +162,7 @@ export default mixins(
} else { } else {
// Everything else saves them directly as an array. // Everything else saves them directly as an array.
newValue = get(this.nodeValues, `${this.path}.${optionName}`, []); newValue = get(this.nodeValues, `${this.path}.${optionName}`, []);
newValue.push(JSON.parse(JSON.stringify(option.default))); newValue.push(deepCopy(option.default));
} }
parameterData = { parameterData = {
@ -172,7 +173,7 @@ export default mixins(
// Add a new option // Add a new option
parameterData = { parameterData = {
name, name,
value: JSON.parse(JSON.stringify(option.default)), value: deepCopy(option.default),
}; };
} }

View file

@ -113,8 +113,10 @@ import {
} from '@/Interface'; } from '@/Interface';
import { import {
deepCopy,
INodeParameters, INodeParameters,
INodePropertyCollection, INodePropertyCollection,
NodeParameterValue,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { get } from 'lodash'; import { get } from 'lodash';
@ -249,13 +251,13 @@ export default mixins(genericHelpers)
// Multiple values are allowed so append option to array // Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []); newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []);
if (Array.isArray(optionParameter.default)) { if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(...JSON.parse(JSON.stringify(optionParameter.default))); (newParameterValue[optionParameter.name] as INodeParameters[]).push(...deepCopy(optionParameter.default as INodeParameters[]));
} else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') { } else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(JSON.parse(JSON.stringify(optionParameter.default))); (newParameterValue[optionParameter.name] as NodeParameterValue[]).push(deepCopy(optionParameter.default));
} }
} else { } else {
// Add a new option // Add a new option
newParameterValue[optionParameter.name] = JSON.parse(JSON.stringify(optionParameter.default)); newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
} }
} }

View file

@ -46,6 +46,7 @@ import { get } from 'lodash';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { deepCopy } from "n8n-workflow";
export default mixins(genericHelpers) export default mixins(genericHelpers)
.extend({ .extend({
@ -87,7 +88,7 @@ export default mixins(genericHelpers)
currentValue = []; currentValue = [];
} }
currentValue.push(JSON.parse(JSON.stringify(this.parameter.default))); currentValue.push(deepCopy(this.parameter.default));
const parameterData = { const parameterData = {
name, name,

View file

@ -122,6 +122,7 @@ import {
INodeProperties, INodeProperties,
NodeHelpers, NodeHelpers,
NodeParameterValue, NodeParameterValue,
deepCopy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface'; import { INodeUi, INodeUpdatePropertiesInformation, IUpdateInformation } from '@/Interface';
@ -427,7 +428,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
// Value should be set // Value should be set
if (typeof value === 'object') { if (typeof value === 'object') {
// @ts-ignore // @ts-ignore
Vue.set(get(this.nodeValues, nameParts.join('.')), lastNamePart, JSON.parse(JSON.stringify(value))); Vue.set(get(this.nodeValues, nameParts.join('.')), lastNamePart, deepCopy(value));
} else { } else {
// @ts-ignore // @ts-ignore
Vue.set(get(this.nodeValues, nameParts.join('.')), lastNamePart, value); Vue.set(get(this.nodeValues, nameParts.join('.')), lastNamePart, value);
@ -500,7 +501,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
// Copy the data because it is the data of vuex so make sure that // Copy the data because it is the data of vuex so make sure that
// we do not edit it directly // we do not edit it directly
nodeParameters = JSON.parse(JSON.stringify(nodeParameters)); nodeParameters = deepCopy(nodeParameters);
for (const parameterName of Object.keys(parameterData.value)) { for (const parameterName of Object.keys(parameterData.value)) {
//@ts-ignore //@ts-ignore
@ -591,7 +592,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
// Copy the data because it is the data of vuex so make sure that // Copy the data because it is the data of vuex so make sure that
// we do not edit it directly // we do not edit it directly
nodeParameters = JSON.parse(JSON.stringify(nodeParameters)); nodeParameters = deepCopy(nodeParameters);
// Remove the 'parameters.' from the beginning to just have the // Remove the 'parameters.' from the beginning to just have the
// actual parameter name // actual parameter name
@ -734,7 +735,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers).extend({
} }
} }
Vue.set(this.nodeValues, 'parameters', JSON.parse(JSON.stringify(this.node.parameters))); Vue.set(this.nodeValues, 'parameters', deepCopy(this.node.parameters));
} else { } else {
this.nodeValid = false; this.nodeValid = false;
} }

View file

@ -97,6 +97,7 @@
<script lang="ts"> <script lang="ts">
import { import {
deepCopy,
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
INodeTypeDescription, INodeTypeDescription,
@ -288,7 +289,7 @@ export default mixins(
if (parameterGotResolved === true) { if (parameterGotResolved === true) {
if (this.path) { if (this.path) {
rawValues = JSON.parse(JSON.stringify(this.nodeValues)); rawValues = deepCopy(this.nodeValues);
set(rawValues, this.path, nodeValues); set(rawValues, this.path, nodeValues);
return this.displayParameter(rawValues, parameter, this.path, this.node); return this.displayParameter(rawValues, parameter, this.path, this.node);
} else { } else {

View file

@ -192,6 +192,7 @@ import { WORKFLOW_SETTINGS_MODAL_KEY } from '../constants';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { mapGetters } from "vuex"; import { mapGetters } from "vuex";
import { deepCopy } from "n8n-workflow";
export default mixins( export default mixins(
externalHooks, externalHooks,
@ -274,7 +275,7 @@ export default mixins(
this.$showError(error, 'Problem loading settings', 'The following error occurred loading the data:'); this.$showError(error, 'Problem loading settings', 'The following error occurred loading the data:');
} }
const workflowSettings = JSON.parse(JSON.stringify(this.$store.getters.workflowSettings)); const workflowSettings = deepCopy(this.$store.getters.workflowSettings);
if (workflowSettings.timezone === undefined) { if (workflowSettings.timezone === undefined) {
workflowSettings.timezone = 'DEFAULT'; workflowSettings.timezone = 'DEFAULT';
@ -536,7 +537,7 @@ export default mixins(
} }
} }
const oldSettings = JSON.parse(JSON.stringify(this.$store.getters.workflowSettings)); const oldSettings = deepCopy(this.$store.getters.workflowSettings);
this.$store.commit('setWorkflowSettings', localWorkflowSettings); this.$store.commit('setWorkflowSettings', localWorkflowSettings);

View file

@ -31,6 +31,7 @@ import {
IExecuteData, IExecuteData,
INodeConnection, INodeConnection,
IWebhookDescription, IWebhookDescription,
deepCopy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -361,8 +362,8 @@ export const workflowHelpers = mixins(
cachedWorkflow = new Workflow({ cachedWorkflow = new Workflow({
id: workflowId, id: workflowId,
name: workflowName, name: workflowName,
nodes: copyData ? JSON.parse(JSON.stringify(nodes)) : nodes, nodes: copyData ? deepCopy(nodes) : nodes,
connections: copyData? JSON.parse(JSON.stringify(connections)): connections, connections: copyData? deepCopy(connections): connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: this.$store.getters.workflowSettings, settings: this.$store.getters.workflowSettings,

View file

@ -99,6 +99,7 @@ import * as CanvasHelpers from './canvasHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { import {
deepCopy,
IConnection, IConnection,
IConnections, IConnections,
IDataObject, IDataObject,
@ -459,7 +460,7 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', data); this.$store.commit('setWorkflowExecutionData', data);
this.$store.commit('setWorkflowPinData', data.workflowData.pinData); this.$store.commit('setWorkflowPinData', data.workflowData.pinData);
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections))); await this.addNodes(deepCopy(data.workflowData.nodes), deepCopy(data.workflowData.connections));
this.$nextTick(() => { this.$nextTick(() => {
this.zoomToFit(); this.zoomToFit();
this.$store.commit('setStateDirty', false); this.$store.commit('setStateDirty', false);
@ -2193,7 +2194,7 @@ export default mixins(
// Deep copy the data so that data on lower levels of the node-properties do // Deep copy the data so that data on lower levels of the node-properties do
// not share objects // not share objects
const newNodeData = JSON.parse(JSON.stringify(this.getNodeDataToSave(node))); const newNodeData = deepCopy(this.getNodeDataToSave(node));
newNodeData.id = uuid(); newNodeData.id = uuid();
// Check if node-name is unique else find one that is // Check if node-name is unique else find one that is

View file

@ -16,3 +16,4 @@ export * from './WorkflowDataProxy';
export * from './WorkflowErrors'; export * from './WorkflowErrors';
export * from './WorkflowHooks'; export * from './WorkflowHooks';
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
export { deepCopy } from './utils';

View file

@ -0,0 +1,34 @@
import { deepCopy } from './utils';
describe('deepCopy', () => {
it('should deep copy an object', () => {
const object = {
deep: {
props: {
list: [{ a: 1 }, { b: 2 }, { c: 3 }],
},
arr: [1, 2, 3],
},
arr: [
{
prop: {
list: ['a', 'b', 'c'],
},
},
],
func: () => {},
date: new Date(),
undef: undefined,
nil: null,
bool: true,
num: 1,
};
const copy = deepCopy(object);
expect(copy).toEqual(object);
expect(copy).not.toBe(object);
expect(copy.arr).toEqual(object.arr);
expect(copy.arr).not.toBe(object.arr);
expect(copy.deep.props).toEqual(object.deep.props);
expect(copy.deep.props).not.toBe(object.deep.props);
});
});

View file

@ -1 +1,32 @@
export const deepCopy = <T>(toCopy: T) => JSON.parse(JSON.stringify(toCopy)) as T; /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */
export const deepCopy = <T>(source: T): T => {
let clone: any;
let i: any;
const hasOwnProp = Object.prototype.hasOwnProperty.bind(source);
// Primitives & Null
if (typeof source !== 'object' || source === null) {
return source;
}
// Date
if (source instanceof Date) {
return new Date(source.getTime()) as T;
}
// Array
if (Array.isArray(source)) {
clone = [];
const len = source.length;
for (i = 0; i < len; i++) {
clone[i] = deepCopy(source[i]);
}
return clone;
}
// Object
clone = {};
for (i in source) {
if (hasOwnProp(i)) {
clone[i] = deepCopy((source as any)[i]);
}
}
return clone;
};
// eslint-enable