From 8a804b3b49412e1cf82a7462d8a6dd5e29fb78d4 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: Thu, 7 Nov 2024 11:54:39 +0100 Subject: [PATCH] fix(core): Revert all the context helpers changes (#11616) --- packages/core/src/NodeExecuteFunctions.ts | 22 +- .../execute-single-context.ts | 14 +- .../helpers/__tests__/binary-helpers.test.ts | 136 ------- .../__tests__/scheduling-helpers.test.ts | 33 -- .../__tests__/ssh-tunnel-helpers.test.ts | 32 -- .../helpers/binary-helpers.ts | 148 ------- .../helpers/request-helpers.ts | 381 ------------------ .../helpers/scheduling-helpers.ts | 20 - .../helpers/ssh-tunnel-helpers.ts | 18 - .../node-execution-context/hook-context.ts | 4 +- .../load-options-context.ts | 14 +- .../node-execution-context/poll-context.ts | 12 +- .../node-execution-context/trigger-context.ts | 16 +- .../node-execution-context/webhook-context.ts | 8 +- 14 files changed, 55 insertions(+), 803 deletions(-) delete mode 100644 packages/core/src/node-execution-context/helpers/__tests__/binary-helpers.test.ts delete mode 100644 packages/core/src/node-execution-context/helpers/__tests__/scheduling-helpers.test.ts delete mode 100644 packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts delete mode 100644 packages/core/src/node-execution-context/helpers/binary-helpers.ts delete mode 100644 packages/core/src/node-execution-context/helpers/request-helpers.ts delete mode 100644 packages/core/src/node-execution-context/helpers/scheduling-helpers.ts delete mode 100644 packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 383624b569..e645309644 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -108,6 +108,7 @@ import type { AiEvent, ISupplyDataFunctions, WebhookType, + SchedulingFunctions, } from 'n8n-workflow'; import { NodeConnectionType, @@ -172,6 +173,7 @@ import { TriggerContext, WebhookContext, } from './node-execution-context'; +import { ScheduledTaskManager } from './ScheduledTaskManager'; import { getSecretsProxy } from './Secrets'; import { SSHClientsManager } from './SSHClientsManager'; @@ -3023,7 +3025,7 @@ const executionCancellationFunctions = ( }, }); -const getRequestHelperFunctions = ( +export const getRequestHelperFunctions = ( workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData, @@ -3343,11 +3345,19 @@ const getRequestHelperFunctions = ( }; }; -const getSSHTunnelFunctions = (): SSHTunnelFunctions => ({ +export const getSSHTunnelFunctions = (): SSHTunnelFunctions => ({ getSSHClient: async (credentials) => await Container.get(SSHClientsManager).getClient(credentials), }); +export 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) { @@ -3414,7 +3424,7 @@ export function isFilePathBlocked(filePath: string): boolean { return false; } -const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions => ({ +export const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions => ({ async createReadStream(filePath) { try { await fsAccess(filePath); @@ -3450,7 +3460,7 @@ const getFileSystemHelperFunctions = (node: INode): FileSystemHelperFunctions => }, }); -const getNodeHelperFunctions = ( +export const getNodeHelperFunctions = ( { executionId }: IWorkflowExecuteAdditionalData, workflowId: string, ): NodeHelperFunctions => ({ @@ -3458,7 +3468,7 @@ const getNodeHelperFunctions = ( await copyBinaryFile(workflowId, executionId!, filePath, fileName, mimeType), }); -const getBinaryHelperFunctions = ( +export const getBinaryHelperFunctions = ( { executionId }: IWorkflowExecuteAdditionalData, workflowId: string, ): BinaryHelperFunctions => ({ @@ -3476,7 +3486,7 @@ const getBinaryHelperFunctions = ( }, }); -const getCheckProcessedHelperFunctions = ( +export const getCheckProcessedHelperFunctions = ( workflow: Workflow, node: INode, ): DeduplicationHelperFunctions => ({ diff --git a/packages/core/src/node-execution-context/execute-single-context.ts b/packages/core/src/node-execution-context/execute-single-context.ts index 6d8ef2a083..2b03a81974 100644 --- a/packages/core/src/node-execution-context/execute-single-context.ts +++ b/packages/core/src/node-execution-context/execute-single-context.ts @@ -27,13 +27,13 @@ import { continueOnFail, getAdditionalKeys, getBinaryDataBuffer, + getBinaryHelperFunctions, getCredentials, getNodeParameter, + getRequestHelperFunctions, returnJsonArray, } from '@/NodeExecuteFunctions'; -import { BinaryHelpers } from './helpers/binary-helpers'; -import { RequestHelpers } from './helpers/request-helpers'; import { NodeExecutionContext } from './node-execution-context'; export class ExecuteSingleContext extends NodeExecutionContext implements IExecuteSingleFunctions { @@ -57,8 +57,14 @@ export class ExecuteSingleContext extends NodeExecutionContext implements IExecu this.helpers = { createDeferredPromise, returnJsonArray, - ...new BinaryHelpers(workflow, additionalData).exported, - ...new RequestHelpers(this, workflow, node, additionalData).exported, + ...getRequestHelperFunctions( + workflow, + node, + additionalData, + runExecutionData, + connectionInputData, + ), + ...getBinaryHelperFunctions(additionalData, workflow.id), assertBinaryData: (propertyName, inputIndex = 0) => assertBinaryData(inputData, node, itemIndex, propertyName, inputIndex), diff --git a/packages/core/src/node-execution-context/helpers/__tests__/binary-helpers.test.ts b/packages/core/src/node-execution-context/helpers/__tests__/binary-helpers.test.ts deleted file mode 100644 index 302713954f..0000000000 --- a/packages/core/src/node-execution-context/helpers/__tests__/binary-helpers.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import FileType from 'file-type'; -import { IncomingMessage, type ClientRequest } from 'http'; -import { mock } from 'jest-mock-extended'; -import type { Workflow, IWorkflowExecuteAdditionalData, IBinaryData } from 'n8n-workflow'; -import type { Socket } from 'net'; -import { Container } from 'typedi'; - -import { BinaryDataService } from '@/BinaryData/BinaryData.service'; - -import { BinaryHelpers } from '../binary-helpers'; - -jest.mock('file-type'); - -describe('BinaryHelpers', () => { - let binaryDataService = mock(); - Container.set(BinaryDataService, binaryDataService); - const workflow = mock({ id: '123' }); - const additionalData = mock({ executionId: '456' }); - const binaryHelpers = new BinaryHelpers(workflow, additionalData); - - beforeEach(() => { - jest.clearAllMocks(); - - binaryDataService.store.mockImplementation( - async (_workflowId, _executionId, _buffer, value) => value, - ); - }); - - describe('getBinaryPath', () => { - it('should call getPath method of BinaryDataService', () => { - binaryHelpers.getBinaryPath('mock-binary-data-id'); - expect(binaryDataService.getPath).toHaveBeenCalledWith('mock-binary-data-id'); - }); - }); - - describe('getBinaryMetadata', () => { - it('should call getMetadata method of BinaryDataService', async () => { - await binaryHelpers.getBinaryMetadata('mock-binary-data-id'); - expect(binaryDataService.getMetadata).toHaveBeenCalledWith('mock-binary-data-id'); - }); - }); - - describe('getBinaryStream', () => { - it('should call getStream method of BinaryDataService', async () => { - await binaryHelpers.getBinaryStream('mock-binary-data-id'); - expect(binaryDataService.getAsStream).toHaveBeenCalledWith('mock-binary-data-id', undefined); - }); - }); - - describe('prepareBinaryData', () => { - it('should guess the mime type and file extension if not provided', async () => { - const buffer = Buffer.from('test'); - const fileTypeData = { mime: 'application/pdf', ext: 'pdf' }; - (FileType.fromBuffer as jest.Mock).mockResolvedValue(fileTypeData); - - const binaryData = await binaryHelpers.prepareBinaryData(buffer); - - expect(binaryData.mimeType).toEqual('application/pdf'); - expect(binaryData.fileExtension).toEqual('pdf'); - expect(binaryData.fileType).toEqual('pdf'); - expect(binaryData.fileName).toBeUndefined(); - expect(binaryData.directory).toBeUndefined(); - expect(binaryDataService.store).toHaveBeenCalledWith( - workflow.id, - additionalData.executionId!, - buffer, - binaryData, - ); - }); - - it('should use the provided mime type and file extension if provided', async () => { - const buffer = Buffer.from('test'); - const mimeType = 'application/octet-stream'; - - const binaryData = await binaryHelpers.prepareBinaryData(buffer, undefined, mimeType); - - expect(binaryData.mimeType).toEqual(mimeType); - expect(binaryData.fileExtension).toEqual('bin'); - expect(binaryData.fileType).toBeUndefined(); - expect(binaryData.fileName).toBeUndefined(); - expect(binaryData.directory).toBeUndefined(); - expect(binaryDataService.store).toHaveBeenCalledWith( - workflow.id, - additionalData.executionId!, - buffer, - binaryData, - ); - }); - - const mockSocket = mock({ readableHighWaterMark: 0 }); - - it('should use the contentDisposition.filename, responseUrl, and contentType properties to set the fileName, directory, and mimeType properties of the binaryData object', async () => { - const incomingMessage = new IncomingMessage(mockSocket); - incomingMessage.contentDisposition = { filename: 'test.txt', type: 'attachment' }; - incomingMessage.contentType = 'text/plain'; - incomingMessage.responseUrl = 'https://example.com/test.txt'; - - const binaryData = await binaryHelpers.prepareBinaryData(incomingMessage); - - expect(binaryData.fileName).toEqual('test.txt'); - expect(binaryData.fileType).toEqual('text'); - expect(binaryData.directory).toBeUndefined(); - expect(binaryData.mimeType).toEqual('text/plain'); - expect(binaryData.fileExtension).toEqual('txt'); - }); - - it('should use the req.path property to set the fileName property of the binaryData object if contentDisposition.filename and responseUrl are not provided', async () => { - const incomingMessage = new IncomingMessage(mockSocket); - incomingMessage.contentType = 'text/plain'; - incomingMessage.req = mock({ path: '/test.txt' }); - - const binaryData = await binaryHelpers.prepareBinaryData(incomingMessage); - - expect(binaryData.fileName).toEqual('test.txt'); - expect(binaryData.directory).toBeUndefined(); - expect(binaryData.mimeType).toEqual('text/plain'); - expect(binaryData.fileExtension).toEqual('txt'); - }); - }); - - describe('setBinaryDataBuffer', () => { - it('should call store method of BinaryDataService', async () => { - const binaryData = mock(); - const bufferOrStream = mock(); - - await binaryHelpers.setBinaryDataBuffer(binaryData, bufferOrStream); - - expect(binaryDataService.store).toHaveBeenCalledWith( - workflow.id, - additionalData.executionId, - bufferOrStream, - binaryData, - ); - }); - }); -}); diff --git a/packages/core/src/node-execution-context/helpers/__tests__/scheduling-helpers.test.ts b/packages/core/src/node-execution-context/helpers/__tests__/scheduling-helpers.test.ts deleted file mode 100644 index 06abae8204..0000000000 --- a/packages/core/src/node-execution-context/helpers/__tests__/scheduling-helpers.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { mock } from 'jest-mock-extended'; -import type { Workflow } from 'n8n-workflow'; -import { Container } from 'typedi'; - -import { ScheduledTaskManager } from '@/ScheduledTaskManager'; - -import { SchedulingHelpers } from '../scheduling-helpers'; - -describe('SchedulingHelpers', () => { - const scheduledTaskManager = mock(); - Container.set(ScheduledTaskManager, scheduledTaskManager); - const workflow = mock(); - const schedulingHelpers = new SchedulingHelpers(workflow); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('registerCron', () => { - it('should call registerCron method of ScheduledTaskManager', () => { - const cronExpression = '* * * * * *'; - const onTick = jest.fn(); - - schedulingHelpers.registerCron(cronExpression, onTick); - - expect(scheduledTaskManager.registerCron).toHaveBeenCalledWith( - workflow, - cronExpression, - onTick, - ); - }); - }); -}); 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 deleted file mode 100644 index cbe6916eea..0000000000 --- a/packages/core/src/node-execution-context/helpers/__tests__/ssh-tunnel-helpers.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -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/binary-helpers.ts b/packages/core/src/node-execution-context/helpers/binary-helpers.ts deleted file mode 100644 index a15c59139b..0000000000 --- a/packages/core/src/node-execution-context/helpers/binary-helpers.ts +++ /dev/null @@ -1,148 +0,0 @@ -import FileType from 'file-type'; -import { IncomingMessage } from 'http'; -import MimeTypes from 'mime-types'; -import { ApplicationError, fileTypeFromMimeType } from 'n8n-workflow'; -import type { - BinaryHelperFunctions, - IWorkflowExecuteAdditionalData, - Workflow, - IBinaryData, -} from 'n8n-workflow'; -import path from 'path'; -import type { Readable } from 'stream'; -import Container from 'typedi'; - -import { BinaryDataService } from '@/BinaryData/BinaryData.service'; -import { binaryToBuffer } from '@/BinaryData/utils'; -// eslint-disable-next-line import/no-cycle -import { binaryToString } from '@/NodeExecuteFunctions'; - -export class BinaryHelpers { - private readonly binaryDataService = Container.get(BinaryDataService); - - constructor( - private readonly workflow: Workflow, - private readonly additionalData: IWorkflowExecuteAdditionalData, - ) {} - - get exported(): BinaryHelperFunctions { - return { - getBinaryPath: this.getBinaryPath.bind(this), - getBinaryMetadata: this.getBinaryMetadata.bind(this), - getBinaryStream: this.getBinaryStream.bind(this), - binaryToBuffer, - binaryToString, - prepareBinaryData: this.prepareBinaryData.bind(this), - setBinaryDataBuffer: this.setBinaryDataBuffer.bind(this), - copyBinaryFile: this.copyBinaryFile.bind(this), - }; - } - - getBinaryPath(binaryDataId: string) { - return this.binaryDataService.getPath(binaryDataId); - } - - async getBinaryMetadata(binaryDataId: string) { - return await this.binaryDataService.getMetadata(binaryDataId); - } - - async getBinaryStream(binaryDataId: string, chunkSize?: number) { - return await this.binaryDataService.getAsStream(binaryDataId, chunkSize); - } - - // eslint-disable-next-line complexity - async prepareBinaryData(binaryData: Buffer | Readable, filePath?: string, mimeType?: string) { - let fileExtension: string | undefined; - if (binaryData instanceof IncomingMessage) { - if (!filePath) { - try { - const { responseUrl } = binaryData; - filePath = - binaryData.contentDisposition?.filename ?? - ((responseUrl && new URL(responseUrl).pathname) ?? binaryData.req?.path)?.slice(1); - } catch {} - } - if (!mimeType) { - mimeType = binaryData.contentType; - } - } - - if (!mimeType) { - // If no mime type is given figure it out - - if (filePath) { - // Use file path to guess mime type - const mimeTypeLookup = MimeTypes.lookup(filePath); - if (mimeTypeLookup) { - mimeType = mimeTypeLookup; - } - } - - if (!mimeType) { - if (Buffer.isBuffer(binaryData)) { - // Use buffer to guess mime type - const fileTypeData = await FileType.fromBuffer(binaryData); - if (fileTypeData) { - mimeType = fileTypeData.mime; - fileExtension = fileTypeData.ext; - } - } else if (binaryData instanceof IncomingMessage) { - mimeType = binaryData.headers['content-type']; - } else { - // TODO: detect filetype from other kind of streams - } - } - } - - if (!fileExtension && mimeType) { - fileExtension = MimeTypes.extension(mimeType) || undefined; - } - - if (!mimeType) { - // Fall back to text - mimeType = 'text/plain'; - } - - const returnData: IBinaryData = { - mimeType, - fileType: fileTypeFromMimeType(mimeType), - fileExtension, - data: '', - }; - - if (filePath) { - if (filePath.includes('?')) { - // Remove maybe present query parameters - filePath = filePath.split('?').shift(); - } - - const filePathParts = path.parse(filePath as string); - - if (filePathParts.dir !== '') { - returnData.directory = filePathParts.dir; - } - returnData.fileName = filePathParts.base; - - // Remove the dot - const extractedFileExtension = filePathParts.ext.slice(1); - if (extractedFileExtension) { - returnData.fileExtension = extractedFileExtension; - } - } - - return await this.setBinaryDataBuffer(returnData, binaryData); - } - - async setBinaryDataBuffer(binaryData: IBinaryData, bufferOrStream: Buffer | Readable) { - return await this.binaryDataService.store( - this.workflow.id, - this.additionalData.executionId!, - bufferOrStream, - binaryData, - ); - } - - async copyBinaryFile(): Promise { - throw new ApplicationError('`copyBinaryFile` has been removed. Please upgrade this node.'); - } -} diff --git a/packages/core/src/node-execution-context/helpers/request-helpers.ts b/packages/core/src/node-execution-context/helpers/request-helpers.ts deleted file mode 100644 index 2c5eb19290..0000000000 --- a/packages/core/src/node-execution-context/helpers/request-helpers.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { createHash } from 'crypto'; -import { pick } from 'lodash'; -import { jsonParse, NodeOperationError, sleep } from 'n8n-workflow'; -import type { - RequestHelperFunctions, - IAdditionalCredentialOptions, - IAllExecuteFunctions, - IExecuteData, - IHttpRequestOptions, - IN8nHttpFullResponse, - IN8nHttpResponse, - INode, - INodeExecutionData, - IOAuth2Options, - IRequestOptions, - IRunExecutionData, - IWorkflowDataProxyAdditionalKeys, - IWorkflowExecuteAdditionalData, - NodeParameterValueType, - PaginationOptions, - Workflow, - WorkflowExecuteMode, -} from 'n8n-workflow'; -import { Readable } from 'stream'; - -// eslint-disable-next-line import/no-cycle -import { - applyPaginationRequestData, - binaryToString, - httpRequest, - httpRequestWithAuthentication, - proxyRequestToAxios, - requestOAuth1, - requestOAuth2, - requestWithAuthentication, - validateUrl, -} from '@/NodeExecuteFunctions'; - -export class RequestHelpers { - constructor( - private readonly context: IAllExecuteFunctions, - private readonly workflow: Workflow, - private readonly node: INode, - private readonly additionalData: IWorkflowExecuteAdditionalData, - private readonly runExecutionData: IRunExecutionData | null = null, - private readonly connectionInputData: INodeExecutionData[] = [], - ) {} - - get exported(): RequestHelperFunctions { - return { - httpRequest, - httpRequestWithAuthentication: this.httpRequestWithAuthentication.bind(this), - requestWithAuthenticationPaginated: this.requestWithAuthenticationPaginated.bind(this), - request: this.request.bind(this), - requestWithAuthentication: this.requestWithAuthentication.bind(this), - requestOAuth1: this.requestOAuth1.bind(this), - requestOAuth2: this.requestOAuth2.bind(this), - }; - } - - get httpRequest() { - return httpRequest; - } - - async httpRequestWithAuthentication( - credentialsType: string, - requestOptions: IHttpRequestOptions, - additionalCredentialOptions?: IAdditionalCredentialOptions, - ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await httpRequestWithAuthentication.call( - this.context, - credentialsType, - requestOptions, - this.workflow, - this.node, - this.additionalData, - additionalCredentialOptions, - ); - } - - // eslint-disable-next-line complexity - async requestWithAuthenticationPaginated( - requestOptions: IRequestOptions, - itemIndex: number, - paginationOptions: PaginationOptions, - credentialsType?: string, - additionalCredentialOptions?: IAdditionalCredentialOptions, - ): Promise { - const responseData = []; - if (!requestOptions.qs) { - requestOptions.qs = {}; - } - requestOptions.resolveWithFullResponse = true; - requestOptions.simple = false; - - let tempResponseData: IN8nHttpFullResponse; - let makeAdditionalRequest: boolean; - let paginateRequestData: PaginationOptions['request']; - - const runIndex = 0; - - const additionalKeys = { - $request: requestOptions, - $response: {} as IN8nHttpFullResponse, - $version: this.node.typeVersion, - $pageCount: 0, - }; - - const executeData: IExecuteData = { - data: {}, - node: this.node, - source: null, - }; - - const hashData = { - identicalCount: 0, - previousLength: 0, - previousHash: '', - }; - - do { - paginateRequestData = this.getResolvedValue( - paginationOptions.request as unknown as NodeParameterValueType, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as object as PaginationOptions['request']; - - const tempRequestOptions = applyPaginationRequestData(requestOptions, paginateRequestData); - - if (!validateUrl(tempRequestOptions.uri as string)) { - throw new NodeOperationError( - this.node, - `'${paginateRequestData.url}' is not a valid URL.`, - { - itemIndex, - runIndex, - type: 'invalid_url', - }, - ); - } - - if (credentialsType) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - tempResponseData = await this.requestWithAuthentication( - credentialsType, - tempRequestOptions, - additionalCredentialOptions, - ); - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - tempResponseData = await this.request(tempRequestOptions); - } - - const newResponse: IN8nHttpFullResponse = Object.assign( - { - body: {}, - headers: {}, - statusCode: 0, - }, - pick(tempResponseData, ['body', 'headers', 'statusCode']), - ); - - let contentBody: Exclude; - - if (newResponse.body instanceof Readable && paginationOptions.binaryResult !== true) { - // Keep the original string version that we can use it to hash if needed - contentBody = await binaryToString(newResponse.body as Buffer | Readable); - - const responseContentType = newResponse.headers['content-type']?.toString() ?? ''; - if (responseContentType.includes('application/json')) { - newResponse.body = jsonParse(contentBody, { fallbackValue: {} }); - } else { - newResponse.body = contentBody; - } - tempResponseData.__bodyResolved = true; - tempResponseData.body = newResponse.body; - } else { - contentBody = newResponse.body; - } - - if (paginationOptions.binaryResult !== true || tempResponseData.headers.etag) { - // If the data is not binary (and so not a stream), or an etag is present, - // we check via etag or hash if identical data is received - - let contentLength = 0; - if ('content-length' in tempResponseData.headers) { - contentLength = parseInt(tempResponseData.headers['content-length'] as string) || 0; - } - - if (hashData.previousLength === contentLength) { - let hash: string; - if (tempResponseData.headers.etag) { - // If an etag is provided, we use it as "hash" - hash = tempResponseData.headers.etag as string; - } else { - // If there is no etag, we calculate a hash from the data in the body - if (typeof contentBody !== 'string') { - contentBody = JSON.stringify(contentBody); - } - hash = createHash('md5').update(contentBody).digest('base64'); - } - - if (hashData.previousHash === hash) { - hashData.identicalCount += 1; - if (hashData.identicalCount > 2) { - // Length was identical 5x and hash 3x - throw new NodeOperationError( - this.node, - 'The returned response was identical 5x, so requests got stopped', - { - itemIndex, - description: - 'Check if "Pagination Completed When" has been configured correctly.', - }, - ); - } - } else { - hashData.identicalCount = 0; - } - hashData.previousHash = hash; - } else { - hashData.identicalCount = 0; - } - hashData.previousLength = contentLength; - } - - responseData.push(tempResponseData); - - additionalKeys.$response = newResponse; - additionalKeys.$pageCount = additionalKeys.$pageCount + 1; - - const maxRequests = this.getResolvedValue( - paginationOptions.maxRequests, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as number; - - if (maxRequests && additionalKeys.$pageCount >= maxRequests) { - break; - } - - makeAdditionalRequest = this.getResolvedValue( - paginationOptions.continue, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as boolean; - - if (makeAdditionalRequest) { - if (paginationOptions.requestInterval) { - const requestInterval = this.getResolvedValue( - paginationOptions.requestInterval, - itemIndex, - runIndex, - executeData, - additionalKeys, - false, - ) as number; - - await sleep(requestInterval); - } - if (tempResponseData.statusCode < 200 || tempResponseData.statusCode >= 300) { - // We have it configured to let all requests pass no matter the response code - // via "requestOptions.simple = false" to not by default fail if it is for example - // configured to stop on 404 response codes. For that reason we have to throw here - // now an error manually if the response code is not a success one. - let data = tempResponseData.body; - if (data instanceof Readable && paginationOptions.binaryResult !== true) { - data = await binaryToString(data as Buffer | Readable); - } else if (typeof data === 'object') { - data = JSON.stringify(data); - } - - throw Object.assign(new Error(`${tempResponseData.statusCode} - "${data?.toString()}"`), { - statusCode: tempResponseData.statusCode, - error: data, - isAxiosError: true, - response: { - headers: tempResponseData.headers, - status: tempResponseData.statusCode, - statusText: tempResponseData.statusMessage, - }, - }); - } - } - } while (makeAdditionalRequest); - - return responseData; - } - - async request(uriOrObject: string | IRequestOptions, options?: IRequestOptions) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await proxyRequestToAxios( - this.workflow, - this.additionalData, - this.node, - uriOrObject, - options, - ); - } - - async requestWithAuthentication( - credentialsType: string, - requestOptions: IRequestOptions, - additionalCredentialOptions?: IAdditionalCredentialOptions, - itemIndex?: number, - ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await requestWithAuthentication.call( - this.context, - credentialsType, - requestOptions, - this.workflow, - this.node, - this.additionalData, - additionalCredentialOptions, - itemIndex, - ); - } - - async requestOAuth1(credentialsType: string, requestOptions: IRequestOptions) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await requestOAuth1.call(this.context, credentialsType, requestOptions); - } - - async requestOAuth2( - credentialsType: string, - requestOptions: IRequestOptions, - oAuth2Options?: IOAuth2Options, - ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return await requestOAuth2.call( - this.context, - credentialsType, - requestOptions, - this.node, - this.additionalData, - oAuth2Options, - ); - } - - private getResolvedValue( - parameterValue: NodeParameterValueType, - itemIndex: number, - runIndex: number, - executeData: IExecuteData, - additionalKeys?: IWorkflowDataProxyAdditionalKeys, - returnObjectAsString = false, - ): NodeParameterValueType { - const mode: WorkflowExecuteMode = 'internal'; - - if ( - typeof parameterValue === 'object' || - (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') - ) { - return this.workflow.expression.getParameterValue( - parameterValue, - this.runExecutionData, - runIndex, - itemIndex, - this.node.name, - this.connectionInputData, - mode, - additionalKeys ?? {}, - executeData, - returnObjectAsString, - ); - } - - return parameterValue; - } -} diff --git a/packages/core/src/node-execution-context/helpers/scheduling-helpers.ts b/packages/core/src/node-execution-context/helpers/scheduling-helpers.ts deleted file mode 100644 index e193f2beaf..0000000000 --- a/packages/core/src/node-execution-context/helpers/scheduling-helpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { CronExpression, Workflow, SchedulingFunctions } from 'n8n-workflow'; -import { Container } from 'typedi'; - -import { ScheduledTaskManager } from '@/ScheduledTaskManager'; - -export class SchedulingHelpers { - private readonly scheduledTaskManager = Container.get(ScheduledTaskManager); - - constructor(private readonly workflow: Workflow) {} - - get exported(): SchedulingFunctions { - return { - registerCron: this.registerCron.bind(this), - }; - } - - registerCron(cronExpression: CronExpression, onTick: () => void) { - this.scheduledTaskManager.registerCron(this.workflow, cronExpression, onTick); - } -} 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 deleted file mode 100644 index f44df0e166..0000000000 --- a/packages/core/src/node-execution-context/helpers/ssh-tunnel-helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/hook-context.ts b/packages/core/src/node-execution-context/hook-context.ts index 7cc6567779..5585d6b8f3 100644 --- a/packages/core/src/node-execution-context/hook-context.ts +++ b/packages/core/src/node-execution-context/hook-context.ts @@ -21,10 +21,10 @@ import { getCredentials, getNodeParameter, getNodeWebhookUrl, + getRequestHelperFunctions, getWebhookDescription, } from '@/NodeExecuteFunctions'; -import { RequestHelpers } from './helpers/request-helpers'; import { NodeExecutionContext } from './node-execution-context'; export class HookContext extends NodeExecutionContext implements IHookFunctions { @@ -40,7 +40,7 @@ export class HookContext extends NodeExecutionContext implements IHookFunctions ) { super(workflow, node, additionalData, mode); - this.helpers = new RequestHelpers(this, workflow, node, additionalData); + this.helpers = getRequestHelperFunctions(workflow, node, additionalData); } getActivationMode() { diff --git a/packages/core/src/node-execution-context/load-options-context.ts b/packages/core/src/node-execution-context/load-options-context.ts index 98dd58210b..bb43d9c2e2 100644 --- a/packages/core/src/node-execution-context/load-options-context.ts +++ b/packages/core/src/node-execution-context/load-options-context.ts @@ -13,10 +13,14 @@ import type { import { extractValue } from '@/ExtractValue'; // eslint-disable-next-line import/no-cycle -import { getAdditionalKeys, getCredentials, getNodeParameter } from '@/NodeExecuteFunctions'; +import { + getAdditionalKeys, + getCredentials, + getNodeParameter, + getRequestHelperFunctions, + getSSHTunnelFunctions, +} from '@/NodeExecuteFunctions'; -import { RequestHelpers } from './helpers/request-helpers'; -import { SSHTunnelHelpers } from './helpers/ssh-tunnel-helpers'; import { NodeExecutionContext } from './node-execution-context'; export class LoadOptionsContext extends NodeExecutionContext implements ILoadOptionsFunctions { @@ -31,8 +35,8 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt super(workflow, node, additionalData, 'internal'); this.helpers = { - ...new RequestHelpers(this, workflow, node, additionalData).exported, - ...new SSHTunnelHelpers().exported, + ...getSSHTunnelFunctions(), + ...getRequestHelperFunctions(workflow, node, additionalData), }; } diff --git a/packages/core/src/node-execution-context/poll-context.ts b/packages/core/src/node-execution-context/poll-context.ts index 88e8caafc8..e3c0dd0cc8 100644 --- a/packages/core/src/node-execution-context/poll-context.ts +++ b/packages/core/src/node-execution-context/poll-context.ts @@ -16,14 +16,14 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow'; // eslint-disable-next-line import/no-cycle import { getAdditionalKeys, + getBinaryHelperFunctions, getCredentials, getNodeParameter, + getRequestHelperFunctions, + getSchedulingFunctions, returnJsonArray, } from '@/NodeExecuteFunctions'; -import { BinaryHelpers } from './helpers/binary-helpers'; -import { RequestHelpers } from './helpers/request-helpers'; -import { SchedulingHelpers } from './helpers/scheduling-helpers'; import { NodeExecutionContext } from './node-execution-context'; const throwOnEmit = () => { @@ -51,9 +51,9 @@ export class PollContext extends NodeExecutionContext implements IPollFunctions this.helpers = { createDeferredPromise, returnJsonArray, - ...new BinaryHelpers(workflow, additionalData).exported, - ...new RequestHelpers(this, workflow, node, additionalData).exported, - ...new SchedulingHelpers(workflow).exported, + ...getRequestHelperFunctions(workflow, node, additionalData), + ...getBinaryHelperFunctions(additionalData, workflow.id), + ...getSchedulingFunctions(workflow), }; } diff --git a/packages/core/src/node-execution-context/trigger-context.ts b/packages/core/src/node-execution-context/trigger-context.ts index 8535ccfe6c..5ae6ce47df 100644 --- a/packages/core/src/node-execution-context/trigger-context.ts +++ b/packages/core/src/node-execution-context/trigger-context.ts @@ -16,15 +16,15 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow'; // eslint-disable-next-line import/no-cycle import { getAdditionalKeys, + getBinaryHelperFunctions, getCredentials, getNodeParameter, + getRequestHelperFunctions, + getSchedulingFunctions, + getSSHTunnelFunctions, 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 = () => { @@ -52,10 +52,10 @@ export class TriggerContext extends NodeExecutionContext implements ITriggerFunc this.helpers = { createDeferredPromise, returnJsonArray, - ...new BinaryHelpers(workflow, additionalData).exported, - ...new RequestHelpers(this, workflow, node, additionalData).exported, - ...new SchedulingHelpers(workflow).exported, - ...new SSHTunnelHelpers().exported, + ...getSSHTunnelFunctions(), + ...getRequestHelperFunctions(workflow, node, additionalData), + ...getBinaryHelperFunctions(additionalData, workflow.id), + ...getSchedulingFunctions(workflow), }; } diff --git a/packages/core/src/node-execution-context/webhook-context.ts b/packages/core/src/node-execution-context/webhook-context.ts index a7fa7203c8..4d3eef53e2 100644 --- a/packages/core/src/node-execution-context/webhook-context.ts +++ b/packages/core/src/node-execution-context/webhook-context.ts @@ -24,15 +24,15 @@ import { ApplicationError, createDeferredPromise } from 'n8n-workflow'; import { copyBinaryFile, getAdditionalKeys, + getBinaryHelperFunctions, getCredentials, getInputConnectionData, getNodeParameter, getNodeWebhookUrl, + getRequestHelperFunctions, returnJsonArray, } from '@/NodeExecuteFunctions'; -import { BinaryHelpers } from './helpers/binary-helpers'; -import { RequestHelpers } from './helpers/request-helpers'; import { NodeExecutionContext } from './node-execution-context'; export class WebhookContext extends NodeExecutionContext implements IWebhookFunctions { @@ -54,8 +54,8 @@ export class WebhookContext extends NodeExecutionContext implements IWebhookFunc this.helpers = { createDeferredPromise, returnJsonArray, - ...new BinaryHelpers(workflow, additionalData).exported, - ...new RequestHelpers(this, workflow, node, additionalData).exported, + ...getRequestHelperFunctions(workflow, node, additionalData), + ...getBinaryHelperFunctions(additionalData, workflow.id), }; this.nodeHelpers = {