From 1be7de6180f90c79873dab6d5682f5d82598de2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 4 Nov 2024 11:13:44 +0100 Subject: [PATCH] refactor(core): Extract trigger context out of NodeExecutionFunctions (no-changelog) (#11453) --- packages/cli/src/active-workflow-manager.ts | 23 +++-- packages/core/src/NodeExecuteFunctions.ts | 65 +------------ .../__tests__/trigger-context.test.ts | 96 +++++++++++++++++++ .../__tests__/ssh-tunnel-helpers.test.ts | 32 +++++++ .../helpers/ssh-tunnel-helpers.ts | 18 ++++ .../core/src/node-execution-context/index.ts | 1 + .../node-execution-context/trigger-context.ts | 96 +++++++++++++++++++ 7 files changed, 256 insertions(+), 75 deletions(-) create mode 100644 packages/core/src/node-execution-context/__tests__/trigger-context.test.ts create mode 100644 packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts create mode 100644 packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts create mode 100644 packages/core/src/node-execution-context/trigger-context.ts diff --git a/packages/cli/src/active-workflow-manager.ts b/packages/cli/src/active-workflow-manager.ts index de3ff6d25e..22cc0f5700 100644 --- a/packages/cli/src/active-workflow-manager.ts +++ b/packages/cli/src/active-workflow-manager.ts @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { ActiveWorkflows, InstanceSettings, NodeExecuteFunctions, PollContext } from 'n8n-core'; +import { + ActiveWorkflows, + InstanceSettings, + NodeExecuteFunctions, + PollContext, + TriggerContext, +} from 'n8n-core'; import type { ExecutionError, IDeferredPromise, @@ -325,18 +331,11 @@ export class ActiveWorkflowManager { activation: WorkflowActivateMode, ): IGetExecuteTriggerFunctions { return (workflow: Workflow, node: INode) => { - const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions( - workflow, - node, - additionalData, - mode, - activation, - ); - returnFunctions.emit = ( + const emit = ( data: INodeExecutionData[][], responsePromise?: IDeferredPromise, donePromise?: IDeferredPromise, - ): void => { + ) => { this.logger.debug(`Received trigger for workflow "${workflow.name}"`); void this.workflowStaticDataService.saveStaticData(workflow); @@ -360,7 +359,7 @@ export class ActiveWorkflowManager { executePromise.catch((error: Error) => this.logger.error(error.message, { error })); } }; - returnFunctions.emitError = (error: Error): void => { + const emitError = (error: Error): void => { this.logger.info( `The trigger node "${node.name}" of workflow "${workflowData.name}" failed with the error: "${error.message}". Will try to reactivate.`, { @@ -385,7 +384,7 @@ export class ActiveWorkflowManager { this.addQueuedWorkflowActivation(activation, workflowData as WorkflowEntity); }; - return returnFunctions; + return new TriggerContext(workflow, node, additionalData, mode, activation, emit, emitError); }; } diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index b6028b9194..978e4bacd9 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -101,7 +101,6 @@ import type { INodeParameters, EnsureTypeOptions, SSHTunnelFunctions, - SchedulingFunctions, DeduplicationHelperFunctions, IDeduplicationOutput, IDeduplicationOutputItems, @@ -168,8 +167,7 @@ import { extractValue } from './ExtractValue'; import { InstanceSettings } from './InstanceSettings'; import type { ExtendedValidationResult, IResponseError } from './Interfaces'; // eslint-disable-next-line import/no-cycle -import { PollContext } from './node-execution-context'; -import { ScheduledTaskManager } from './ScheduledTaskManager'; +import { PollContext, TriggerContext } from './node-execution-context'; import { getSecretsProxy } from './Secrets'; import { SSHClientsManager } from './SSHClientsManager'; @@ -3346,14 +3344,6 @@ const getSSHTunnelFunctions = (): SSHTunnelFunctions => ({ await Container.get(SSHClientsManager).getClient(credentials), }); -const getSchedulingFunctions = (workflow: Workflow): SchedulingFunctions => { - const scheduledTaskManager = Container.get(ScheduledTaskManager); - return { - registerCron: (cronExpression, onTick) => - scheduledTaskManager.registerCron(workflow, cronExpression, onTick), - }; -}; - const getAllowedPaths = () => { const restrictFileAccessTo = process.env[RESTRICT_FILE_ACCESS_TO]; if (!restrictFileAccessTo) { @@ -3571,58 +3561,7 @@ export function getExecuteTriggerFunctions( mode: WorkflowExecuteMode, activation: WorkflowActivateMode, ): ITriggerFunctions { - return ((workflow: Workflow, node: INode) => { - return { - ...getCommonWorkflowFunctions(workflow, node, additionalData), - emit: (): void => { - throw new ApplicationError( - 'Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function', - ); - }, - emitError: (): void => { - throw new ApplicationError( - 'Overwrite NodeExecuteFunctions.getExecuteTriggerFunctions.emit function', - ); - }, - getMode: () => mode, - getActivationMode: () => activation, - getCredentials: async (type) => - await getCredentials(workflow, node, type, additionalData, mode), - getNodeParameter: ( - parameterName: string, - fallbackValue?: any, - options?: IGetNodeParameterOptions, - ): NodeParameterValueType | object => { - const runExecutionData: IRunExecutionData | null = null; - const itemIndex = 0; - const runIndex = 0; - const connectionInputData: INodeExecutionData[] = []; - - return getNodeParameter( - workflow, - runExecutionData, - runIndex, - connectionInputData, - node, - parameterName, - itemIndex, - mode, - getAdditionalKeys(additionalData, mode, runExecutionData), - undefined, - fallbackValue, - options, - ); - }, - helpers: { - createDeferredPromise, - ...getSSHTunnelFunctions(), - ...getRequestHelperFunctions(workflow, node, additionalData), - ...getBinaryHelperFunctions(additionalData, workflow.id), - ...getSchedulingFunctions(workflow), - returnJsonArray, - }, - }; - })(workflow, node); + return new TriggerContext(workflow, node, additionalData, mode, activation); } /** diff --git a/packages/core/src/node-execution-context/__tests__/trigger-context.test.ts b/packages/core/src/node-execution-context/__tests__/trigger-context.test.ts new file mode 100644 index 0000000000..3c91d22e6d --- /dev/null +++ b/packages/core/src/node-execution-context/__tests__/trigger-context.test.ts @@ -0,0 +1,96 @@ +import { mock } from 'jest-mock-extended'; +import type { + Expression, + ICredentialDataDecryptedObject, + ICredentialsHelper, + INode, + INodeType, + INodeTypes, + IWorkflowExecuteAdditionalData, + Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, +} from 'n8n-workflow'; + +import { TriggerContext } from '../trigger-context'; + +describe('TriggerContext', () => { + const testCredentialType = 'testCredential'; + const nodeType = mock({ + description: { + credentials: [ + { + name: testCredentialType, + required: true, + }, + ], + properties: [ + { + name: 'testParameter', + required: true, + }, + ], + }, + }); + const nodeTypes = mock(); + const expression = mock(); + const workflow = mock({ expression, nodeTypes }); + const node = mock({ + credentials: { + [testCredentialType]: { + id: 'testCredentialId', + }, + }, + }); + node.parameters = { + testParameter: 'testValue', + }; + const credentialsHelper = mock(); + const additionalData = mock({ credentialsHelper }); + const mode: WorkflowExecuteMode = 'manual'; + const activation: WorkflowActivateMode = 'init'; + + const triggerContext = new TriggerContext(workflow, node, additionalData, mode, activation); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getActivationMode', () => { + it('should return the activation property', () => { + const result = triggerContext.getActivationMode(); + expect(result).toBe(activation); + }); + }); + + describe('getCredentials', () => { + it('should get decrypted credentials', async () => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + credentialsHelper.getDecrypted.mockResolvedValue({ secret: 'token' }); + + const credentials = + await triggerContext.getCredentials(testCredentialType); + + expect(credentials).toEqual({ secret: 'token' }); + }); + }); + + describe('getNodeParameter', () => { + beforeEach(() => { + nodeTypes.getByNameAndVersion.mockReturnValue(nodeType); + expression.getParameterValue.mockImplementation((value) => value); + }); + + it('should return parameter value when it exists', () => { + const parameter = triggerContext.getNodeParameter('testParameter'); + + expect(parameter).toBe('testValue'); + }); + + it('should return the fallback value when the parameter does not exist', () => { + const parameter = triggerContext.getNodeParameter('otherParameter', 'fallback'); + + expect(parameter).toBe('fallback'); + }); + }); +}); diff --git a/packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts b/packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts new file mode 100644 index 0000000000..cbe6916eea --- /dev/null +++ b/packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts @@ -0,0 +1,32 @@ +import { mock } from 'jest-mock-extended'; +import type { SSHCredentials } from 'n8n-workflow'; +import type { Client } from 'ssh2'; +import { Container } from 'typedi'; + +import { SSHClientsManager } from '@/SSHClientsManager'; + +import { SSHTunnelHelpers } from '../ssh-tunnel-helpers'; + +describe('SSHTunnelHelpers', () => { + const sshClientsManager = mock(); + Container.set(SSHClientsManager, sshClientsManager); + const sshTunnelHelpers = new SSHTunnelHelpers(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getSSHClient', () => { + const credentials = mock(); + + it('should call SSHClientsManager.getClient with the given credentials', async () => { + const mockClient = mock(); + sshClientsManager.getClient.mockResolvedValue(mockClient); + + const client = await sshTunnelHelpers.getSSHClient(credentials); + + expect(sshClientsManager.getClient).toHaveBeenCalledWith(credentials); + expect(client).toBe(mockClient); + }); + }); +}); diff --git a/packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts b/packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts new file mode 100644 index 0000000000..f44df0e166 --- /dev/null +++ b/packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts @@ -0,0 +1,18 @@ +import type { SSHCredentials, SSHTunnelFunctions } from 'n8n-workflow'; +import { Container } from 'typedi'; + +import { SSHClientsManager } from '@/SSHClientsManager'; + +export class SSHTunnelHelpers { + private readonly sshClientsManager = Container.get(SSHClientsManager); + + get exported(): SSHTunnelFunctions { + return { + getSSHClient: this.getSSHClient.bind(this), + }; + } + + async getSSHClient(credentials: SSHCredentials) { + return await this.sshClientsManager.getClient(credentials); + } +} diff --git a/packages/core/src/node-execution-context/index.ts b/packages/core/src/node-execution-context/index.ts index 5182804dee..1a64023850 100644 --- a/packages/core/src/node-execution-context/index.ts +++ b/packages/core/src/node-execution-context/index.ts @@ -1,2 +1,3 @@ // eslint-disable-next-line import/no-cycle export { PollContext } from './poll-context'; +export { TriggerContext } from './trigger-context'; diff --git a/packages/core/src/node-execution-context/trigger-context.ts b/packages/core/src/node-execution-context/trigger-context.ts new file mode 100644 index 0000000000..8535ccfe6c --- /dev/null +++ b/packages/core/src/node-execution-context/trigger-context.ts @@ -0,0 +1,96 @@ +import type { + ICredentialDataDecryptedObject, + IGetNodeParameterOptions, + INode, + INodeExecutionData, + IRunExecutionData, + ITriggerFunctions, + IWorkflowExecuteAdditionalData, + NodeParameterValueType, + Workflow, + WorkflowActivateMode, + WorkflowExecuteMode, +} from 'n8n-workflow'; +import { ApplicationError, createDeferredPromise } from 'n8n-workflow'; + +// eslint-disable-next-line import/no-cycle +import { + getAdditionalKeys, + getCredentials, + getNodeParameter, + returnJsonArray, +} from '@/NodeExecuteFunctions'; + +import { BinaryHelpers } from './helpers/binary-helpers'; +import { RequestHelpers } from './helpers/request-helpers'; +import { SchedulingHelpers } from './helpers/scheduling-helpers'; +import { SSHTunnelHelpers } from './helpers/ssh-tunnel-helpers'; +import { NodeExecutionContext } from './node-execution-context'; + +const throwOnEmit = () => { + throw new ApplicationError('Overwrite TriggerContext.emit function'); +}; + +const throwOnEmitError = () => { + throw new ApplicationError('Overwrite TriggerContext.emitError function'); +}; + +export class TriggerContext extends NodeExecutionContext implements ITriggerFunctions { + readonly helpers: ITriggerFunctions['helpers']; + + constructor( + workflow: Workflow, + node: INode, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, + private readonly activation: WorkflowActivateMode, + readonly emit: ITriggerFunctions['emit'] = throwOnEmit, + readonly emitError: ITriggerFunctions['emitError'] = throwOnEmitError, + ) { + super(workflow, node, additionalData, mode); + + this.helpers = { + createDeferredPromise, + returnJsonArray, + ...new BinaryHelpers(workflow, additionalData).exported, + ...new RequestHelpers(this, workflow, node, additionalData).exported, + ...new SchedulingHelpers(workflow).exported, + ...new SSHTunnelHelpers().exported, + }; + } + + getActivationMode() { + return this.activation; + } + + async getCredentials(type: string) { + return await getCredentials(this.workflow, this.node, type, this.additionalData, this.mode); + } + + getNodeParameter( + parameterName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fallbackValue?: any, + options?: IGetNodeParameterOptions, + ): NodeParameterValueType | object { + const runExecutionData: IRunExecutionData | null = null; + const itemIndex = 0; + const runIndex = 0; + const connectionInputData: INodeExecutionData[] = []; + + return getNodeParameter( + this.workflow, + runExecutionData, + runIndex, + connectionInputData, + this.node, + parameterName, + itemIndex, + this.mode, + getAdditionalKeys(this.additionalData, this.mode, runExecutionData), + undefined, + fallbackValue, + options, + ); + } +}