From c8d009bced937d3015bb0bc273a0b132c9d288f1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Sep 2020 12:16:07 +0200 Subject: [PATCH] :sparkles: Add expression support to credentials --- packages/cli/src/ActiveWorkflowRunner.ts | 4 +- packages/cli/src/CredentialsHelper.ts | 32 ++- packages/cli/src/WebhookHelpers.ts | 14 +- packages/core/src/NodeExecuteFunctions.ts | 10 +- packages/editor-ui/src/components/Node.vue | 2 +- .../src/components/mixins/workflowHelpers.ts | 2 +- packages/workflow/src/Expression.ts | 221 ++++++++++++++++++ packages/workflow/src/NodeHelpers.ts | 12 +- packages/workflow/src/Workflow.ts | 201 +--------------- packages/workflow/src/WorkflowDataProxy.ts | 4 +- packages/workflow/src/index.ts | 1 + packages/workflow/test/Workflow.test.ts | 4 +- 12 files changed, 283 insertions(+), 224 deletions(-) create mode 100644 packages/workflow/src/Expression.ts diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 0d2e9bad3e..a7a4a4cf2e 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -234,7 +234,7 @@ export class ActiveWorkflowRunner { path = node.parameters.path as string; if (node.parameters.path === undefined) { - path = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; + path = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['path']) as string | undefined; if (path === undefined) { // TODO: Use a proper logger @@ -243,7 +243,7 @@ export class ActiveWorkflowRunner { } } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookData.webhookDescription['isFullPath'], false) as boolean; const webhook = { workflowId: webhookData.workflowId, diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 84e368adf3..c0ca748a60 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -5,9 +5,14 @@ import { import { ICredentialDataDecryptedObject, ICredentialsHelper, + INode, INodeParameters, INodeProperties, + INodeType, + INodeTypes, + INodeTypeData, NodeHelpers, + Workflow, } from 'n8n-workflow'; import { @@ -18,6 +23,19 @@ import { } from './'; +const mockNodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (nodeTypes?: INodeTypeData): Promise => { }, + getAll: (): INodeType[] => { + // Does not get used in Workflow so no need to return it + return []; + }, + getByName: (nodeType: string): INodeType | undefined => { + return undefined; + }, +}; + + export class CredentialsHelper extends ICredentialsHelper { /** @@ -107,7 +125,7 @@ export class CredentialsHelper extends ICredentialsHelper { const credentialsProperties = this.getCredentialsProperties(type); // Add the default credential values - const decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject; + let decryptedData = NodeHelpers.getNodeParameters(credentialsProperties, decryptedDataOriginal as INodeParameters, true, false) as ICredentialDataDecryptedObject; if (decryptedDataOriginal.oauthTokenData !== undefined) { // The OAuth data gets removed as it is not defined specifically as a parameter @@ -115,6 +133,18 @@ export class CredentialsHelper extends ICredentialsHelper { decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData; } + const mockNode: INode = { + name: '', + typeVersion: 1, + type: 'mock', + position: [0, 0], + parameters: decryptedData as INodeParameters, + }; + + const workflow = new Workflow({ nodes: [mockNode], connections: {}, active: false, nodeTypes: mockNodeTypes}); + // Resolve expressions if any are set + decryptedData = workflow.expression.getComplexParameterValue(mockNode, decryptedData as INodeParameters, undefined) as ICredentialDataDecryptedObject; + // Load and apply the credentials overwrites if any exist const credentialsOverwrites = CredentialsOverwrites(); return credentialsOverwrites.applyOverwrite(type, decryptedData); diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index ffb5449897..a1b77c7975 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -114,8 +114,8 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } // Get the responseMode - const responseMode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); - const responseCode = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; + const responseMode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseMode'], 'onReceived'); + const responseCode = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseCode'], 200) as number; if (!['onReceived', 'lastNode'].includes(responseMode as string)) { // If the mode is not known we error. Is probably best like that instead of using @@ -173,7 +173,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { await WorkflowHelpers.saveStaticData(workflow); if (webhookData.webhookDescription['responseHeaders'] !== undefined) { - const responseHeaders = workflow.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { + const responseHeaders = workflow.expression.getComplexParameterValue(workflowStartNode, webhookData.webhookDescription['responseHeaders'], undefined) as { entries?: Array<{ name: string; value: string; @@ -325,7 +325,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return data; } - const responseData = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); + const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); if (didSendResponse === false) { let data: IDataObject | IDataObject[]; @@ -340,13 +340,13 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { data = returnData.data!.main[0]![0].json; - const responsePropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); + const responsePropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responsePropertyName'], undefined); if (responsePropertyName !== undefined) { data = get(data, responsePropertyName as string) as IDataObject; } - const responseContentType = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); + const responseContentType = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseContentType'], undefined); if (responseContentType !== undefined) { // Send the webhook response manually to be able to set the content-type @@ -379,7 +379,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { didSendResponse = true; } - const responseBinaryPropertyName = workflow.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); + const responseBinaryPropertyName = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseBinaryPropertyName'], 'data'); if (responseBinaryPropertyName === undefined && didSendResponse === false) { responseCallback(new Error('No "responseBinaryPropertyName" is set.'), {}); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index be0cf64975..5cf38f6a92 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -388,7 +388,7 @@ export function getNodeParameter(workflow: Workflow, runExecutionData: IRunExecu let returnData; try { - returnData = workflow.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + returnData = workflow.expression.getParameterValue(value, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); } catch (e) { e.message += ` [Error in parameter: "${parameterName}"]`; throw e; @@ -434,12 +434,12 @@ export function getNodeWebhookUrl(name: string, workflow: Workflow, node: INode, return undefined; } - const path = workflow.getSimpleParameterValue(node, webhookDescription['path']); + const path = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); if (path === undefined) { return undefined; } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); } @@ -654,7 +654,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx return continueOnFail(node); }, evaluateExpression: (expression: string, itemIndex: number) => { - return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData); }, async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise { // tslint:disable-line:no-any return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); @@ -752,7 +752,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: }, evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; - return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); + return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData); }, getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 13c2fee629..06f9bf8aec 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -134,7 +134,7 @@ export default mixins(nodeBase, workflowHelpers).extend({ } if (this.nodeType !== null && this.nodeType.subtitle !== undefined) { - return this.workflow.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined; + return this.workflow.expression.getSimpleParameterValue(this.data as INode, this.nodeType.subtitle) as string | undefined; } if (this.data.parameters.operation !== undefined) { diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 507d6d31e4..a55ac33625 100644 --- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts @@ -357,7 +357,7 @@ export const workflowHelpers = mixins( connectionInputData = []; } - return workflow.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true); + return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, true); }, // Saves the currently loaded workflow to the database. diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts new file mode 100644 index 0000000000..4df397c6da --- /dev/null +++ b/packages/workflow/src/Expression.ts @@ -0,0 +1,221 @@ + +import { + INode, + INodeExecutionData, + INodeParameters, + IRunExecutionData, + NodeParameterValue, + Workflow, + WorkflowDataProxy, +} from './'; + +// @ts-ignore +import * as tmpl from 'riot-tmpl'; + +// Set it to use double curly brackets instead of single ones +tmpl.brackets.set('{{ }}'); + +// Make sure that it does not always print an error when it could not resolve +// a variable +tmpl.tmpl.errorHandler = () => { }; + + +export class Expression { + + workflow: Workflow; + + constructor(workflow: Workflow) { + this.workflow = workflow; + } + + + /** + * Converts an object to a string in a way to make it clear that + * the value comes from an object + * + * @param {object} value + * @returns {string} + * @memberof Workflow + */ + convertObjectValueToString(value: object): string { + const typeName = Array.isArray(value) ? 'Array' : 'Object'; + return `[${typeName}: ${JSON.stringify(value)}]`; + } + + + + /** + * Resolves the paramter value. If it is an expression it will execute it and + * return the result. For everything simply the supplied value will be returned. + * + * @param {NodeParameterValue} parameterValue + * @param {(IRunExecutionData | null)} runExecutionData + * @param {number} runIndex + * @param {number} itemIndex + * @param {string} activeNodeName + * @param {INodeExecutionData[]} connectionInputData + * @param {boolean} [returnObjectAsString=false] + * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} + * @memberof Workflow + */ + resolveSimpleParameterValue(parameterValue: NodeParameterValue, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { + // Check if it is an expression + if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') { + // Is no expression so return value + return parameterValue; + } + + // Is an expression + + // Remove the equal sign + parameterValue = parameterValue.substr(1); + + // Generate a data proxy which allows to query workflow data + const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); + const data = dataProxy.getDataProxy(); + + // Execute the expression + try { + const returnValue = tmpl.tmpl(parameterValue, data); + if (typeof returnValue === 'function') { + throw new Error('Expression resolved to a function. Please add "()"'); + } else if (returnValue !== null && typeof returnValue === 'object') { + if (returnObjectAsString === true) { + return this.convertObjectValueToString(returnValue); + } + } + return returnValue; + } catch (e) { + throw new Error(`Expression is not valid: ${e.message}`); + } + } + + + + /** + * Resolves value of parameter. But does not work for workflow-data. + * + * @param {INode} node + * @param {(string | undefined)} parameterValue + * @param {string} [defaultValue] + * @returns {(string | undefined)} + * @memberof Workflow + */ + getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, defaultValue?: boolean | number | string): boolean | number | string | undefined { + if (parameterValue === undefined) { + // Value is not set so return the default + return defaultValue; + } + + // Get the value of the node (can be an expression) + const runIndex = 0; + const itemIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + const runData: IRunExecutionData = { + resultData: { + runData: {}, + } + }; + + return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData) as boolean | number | string | undefined; + } + + + + /** + * Resolves value of complex parameter. But does not work for workflow-data. + * + * @param {INode} node + * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue + * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} [defaultValue] + * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} + * @memberof Workflow + */ + getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined { + if (parameterValue === undefined) { + // Value is not set so return the default + return defaultValue; + } + + // Get the value of the node (can be an expression) + const runIndex = 0; + const itemIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + const runData: IRunExecutionData = { + resultData: { + runData: {}, + } + }; + + // Resolve the "outer" main values + const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData); + + // Resolve the "inner" values + return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData); + } + + + + /** + * Returns the resolved node parameter value. If it is an expression it will execute it and + * return the result. If the value to resolve is an array or object it will do the same + * for all of the items and values. + * + * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue + * @param {(IRunExecutionData | null)} runExecutionData + * @param {number} runIndex + * @param {number} itemIndex + * @param {string} activeNodeName + * @param {INodeExecutionData[]} connectionInputData + * @param {boolean} [returnObjectAsString=false] + * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} + * @memberof Workflow + */ + getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { + // Helper function which returns true when the parameter is a complex one or array + const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { + return typeof value === 'object'; + }; + + // Helper function which resolves a parameter value depending on if it is simply or not + const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { + if (isComplexParameter(value)) { + return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); + } else { + return this.resolveSimpleParameterValue(value as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); + } + }; + + // Check if it value is a simple one that we can get it resolved directly + if (!isComplexParameter(parameterValue)) { + return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); + } + + // The parameter value is complex so resolve depending on type + + if (Array.isArray(parameterValue)) { + // Data is an array + const returnData = []; + for (const item of parameterValue) { + returnData.push(resolveParameterValue(item)); + } + + if (returnObjectAsString === true && typeof returnData === 'object') { + return this.convertObjectValueToString(returnData); + } + + return returnData as NodeParameterValue[] | INodeParameters[]; + } else { + // Data is an object + const returnData: INodeParameters = {}; + for (const key of Object.keys(parameterValue)) { + returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key]); + } + + if (returnObjectAsString === true && typeof returnData === 'object') { + return this.convertObjectValueToString(returnData); + } + return returnData; + } + } +} diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 3e38931ae8..f6a54414d5 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -755,7 +755,7 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: const returnData: IWebhookData[] = []; for (const webhookDescription of nodeType.description.webhooks) { - let nodeWebhookPath = workflow.getSimpleParameterValue(node, webhookDescription['path']); + let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); if (nodeWebhookPath === undefined) { // TODO: Use a proper logger console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); @@ -768,10 +768,10 @@ export function getNodeWebhooks(workflow: Workflow, node: INode, additionalData: nodeWebhookPath = nodeWebhookPath.slice(1); } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath); - const httpMethod = workflow.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET'); + const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod'], 'GET'); if (httpMethod === undefined) { // TODO: Use a proper logger @@ -809,7 +809,7 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD const returnData: IWebhookData[] = []; for (const webhookDescription of nodeType.description.webhooks) { - let nodeWebhookPath = workflow.getSimpleParameterValue(node, webhookDescription['path']); + let nodeWebhookPath = workflow.expression.getSimpleParameterValue(node, webhookDescription['path']); if (nodeWebhookPath === undefined) { // TODO: Use a proper logger console.error(`No webhook path could be found for node "${node.name}" in workflow "${workflowId}".`); @@ -822,11 +822,11 @@ export function getNodeWebhooksBasic(workflow: Workflow, node: INode): IWebhookD nodeWebhookPath = nodeWebhookPath.slice(1); } - const isFullPath: boolean = workflow.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; + const isFullPath: boolean = workflow.expression.getSimpleParameterValue(node, webhookDescription['isFullPath'], false) as boolean; const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath); - const httpMethod = workflow.getSimpleParameterValue(node, webhookDescription['httpMethod']); + const httpMethod = workflow.expression.getSimpleParameterValue(node, webhookDescription['httpMethod']); if (httpMethod === undefined) { // TODO: Use a proper logger diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 501e7f0de9..c7208f6a87 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -1,5 +1,6 @@ import { + Expression, IConnections, IGetExecuteTriggerFunctions, INode, @@ -23,21 +24,11 @@ import { NodeParameterValue, ObservableObject, WebhookSetupMethodNames, - WorkflowDataProxy, WorkflowExecuteMode, } from './'; -// @ts-ignore -import * as tmpl from 'riot-tmpl'; import { IConnection, IDataObject, IObservableObject } from './Interfaces'; -// Set it to use double curly brackets instead of single ones -tmpl.brackets.set('{{ }}'); - -// Make sure that it does not always print an error when it could not resolve -// a variable -tmpl.tmpl.errorHandler = () => { }; - export class Workflow { id: string | undefined; @@ -46,6 +37,7 @@ export class Workflow { connectionsBySourceNode: IConnections; connectionsByDestinationNode: IConnections; nodeTypes: INodeTypes; + expression: Expression; active: boolean; settings: IWorkflowSettings; @@ -90,6 +82,8 @@ export class Workflow { this.staticData = ObservableObject.create(parameters.staticData || {}, undefined, { ignoreEmptyOnFirstChild: true }); this.settings = parameters.settings || {}; + + this.expression = new Expression(this); } @@ -147,21 +141,6 @@ export class Workflow { - /** - * Converts an object to a string in a way to make it clear that - * the value comes from an object - * - * @param {object} value - * @returns {string} - * @memberof Workflow - */ - convertObjectValueToString(value: object): string { - const typeName = Array.isArray(value) ? 'Array' : 'Object'; - return `[${typeName}: ${JSON.stringify(value)}]`; - } - - - /** * A workflow can only be activated if it has a node which has either triggers * or webhooks defined. @@ -706,65 +685,6 @@ export class Workflow { - /** - * Resolves value of parameter. But does not work for workflow-data. - * - * @param {INode} node - * @param {(string | undefined)} parameterValue - * @param {string} [defaultValue] - * @returns {(string | undefined)} - * @memberof Workflow - */ - getSimpleParameterValue(node: INode, parameterValue: string | boolean | undefined, defaultValue?: boolean | number | string): boolean | number | string | undefined { - if (parameterValue === undefined) { - // Value is not set so return the default - return defaultValue; - } - - // Get the value of the node (can be an expression) - const runIndex = 0; - const itemIndex = 0; - const connectionInputData: INodeExecutionData[] = []; - const runData: IRunExecutionData = { - resultData: { - runData: {}, - } - }; - - return this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData) as boolean | number | string | undefined; - } - - /** - * Resolves value of complex parameter. But does not work for workflow-data. - * - * @param {INode} node - * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue - * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} [defaultValue] - * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined)} - * @memberof Workflow - */ - getComplexParameterValue(node: INode, parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], defaultValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined = undefined): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | undefined { - if (parameterValue === undefined) { - // Value is not set so return the default - return defaultValue; - } - - // Get the value of the node (can be an expression) - const runIndex = 0; - const itemIndex = 0; - const connectionInputData: INodeExecutionData[] = []; - const runData: IRunExecutionData = { - resultData: { - runData: {}, - } - }; - - // Resolve the "outer" main values - const returnData = this.getParameterValue(parameterValue, runData, runIndex, itemIndex, node.name, connectionInputData); - - // Resolve the "inner" values - return this.getParameterValue(returnData, runData, runIndex, itemIndex, node.name, connectionInputData); - } /** * Returns from which of the given nodes the workflow should get started from @@ -839,119 +759,6 @@ export class Workflow { - /** - * Returns the resolved node parameter value. If it is an expression it will execute it and - * return the result. If the value to resolve is an array or object it will do the same - * for all of the items and values. - * - * @param {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} parameterValue - * @param {(IRunExecutionData | null)} runExecutionData - * @param {number} runIndex - * @param {number} itemIndex - * @param {string} activeNodeName - * @param {INodeExecutionData[]} connectionInputData - * @param {boolean} [returnObjectAsString=false] - * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} - * @memberof Workflow - */ - getParameterValue(parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { - // Helper function which returns true when the parameter is a complex one or array - const isComplexParameter = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { - return typeof value === 'object'; - }; - - // Helper function which resolves a parameter value depending on if it is simply or not - const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => { - if (isComplexParameter(value)) { - return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); - } else { - return this.resolveSimpleParameterValue(value as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); - } - }; - - // Check if it value is a simple one that we can get it resolved directly - if (!isComplexParameter(parameterValue)) { - return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString); - } - - // The parameter value is complex so resolve depending on type - - if (Array.isArray(parameterValue)) { - // Data is an array - const returnData = []; - for (const item of parameterValue) { - returnData.push(resolveParameterValue(item)); - } - - if (returnObjectAsString === true && typeof returnData === 'object') { - return this.convertObjectValueToString(returnData); - } - - return returnData as NodeParameterValue[] | INodeParameters[]; - } else { - // Data is an object - const returnData: INodeParameters = {}; - for (const key of Object.keys(parameterValue)) { - returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key]); - } - - if (returnObjectAsString === true && typeof returnData === 'object') { - return this.convertObjectValueToString(returnData); - } - return returnData; - } - } - - - - /** - * Resolves the paramter value. If it is an expression it will execute it and - * return the result. For everything simply the supplied value will be returned. - * - * @param {NodeParameterValue} parameterValue - * @param {(IRunExecutionData | null)} runExecutionData - * @param {number} runIndex - * @param {number} itemIndex - * @param {string} activeNodeName - * @param {INodeExecutionData[]} connectionInputData - * @param {boolean} [returnObjectAsString=false] - * @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])} - * @memberof Workflow - */ - resolveSimpleParameterValue(parameterValue: NodeParameterValue, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], returnObjectAsString = false): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { - // Check if it is an expression - if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') { - // Is no expression so return value - return parameterValue; - } - - // Is an expression - - // Remove the equal sign - parameterValue = parameterValue.substr(1); - - // Generate a data proxy which allows to query workflow data - const dataProxy = new WorkflowDataProxy(this, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); - const data = dataProxy.getDataProxy(); - - // Execute the expression - try { - const returnValue = tmpl.tmpl(parameterValue, data); - if (typeof returnValue === 'function') { - throw new Error('Expression resolved to a function. Please add "()"'); - } else if (returnValue !== null && typeof returnValue === 'object') { - if (returnObjectAsString === true) { - return this.convertObjectValueToString(returnValue); - } - } - return returnValue; - } catch (e) { - throw new Error(`Expression is not valid: ${e.message}`); - } - } - - - /** * Executes the Webhooks method of the node * diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 8094991f3c..852e28e0c9 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -97,7 +97,7 @@ export class WorkflowDataProxy { if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') { // The found value is an expression so resolve it - return that.workflow.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData); + return that.workflow.expression.getParameterValue(returnValue, that.runExecutionData, that.runIndex, that.itemIndex, that.activeNodeName, that.connectionInputData); } return returnValue; @@ -337,7 +337,7 @@ export class WorkflowDataProxy { $env: this.envGetter(), $evaluateExpression: (expression: string, itemIndex?: number) => { itemIndex = itemIndex || that.itemIndex; - return that.workflow.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData); + return that.workflow.expression.getParameterValue('=' + expression, that.runExecutionData, that.runIndex, itemIndex, that.activeNodeName, that.connectionInputData); }, $item: (itemIndex: number, runIndex?: number) => { const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex; diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 89d01313d6..b2c24d55fb 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -1,4 +1,5 @@ export * from './Interfaces'; +export * from './Expression'; export * from './Workflow'; export * from './WorkflowDataProxy'; export * from './WorkflowHooks'; diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index 51b092bef7..61897734b0 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -1097,7 +1097,7 @@ describe('Workflow', () => { for (const parameterName of Object.keys(testData.output)) { const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName]; - const result = workflow.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); + const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); // @ts-ignore expect(result).toEqual(testData.output[parameterName]); } @@ -1247,7 +1247,7 @@ describe('Workflow', () => { const parameterName = 'values'; const parameterValue = nodes.find((node) => node.name === activeNodeName)!.parameters[parameterName]; - const result = workflow.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); + const result = workflow.expression.getParameterValue(parameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData); expect(result).toEqual({ string: [