From 2728f7e5921f6caaedb81639bc018d2c1a4bd531 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 7 Nov 2024 10:19:31 +0000 Subject: [PATCH 01/14] docs: Fix typo in Update action description for Google Storage (#11615) Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> --- .../nodes-base/nodes/Google/CloudStorage/BucketDescription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts b/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts index 0cf0aabc9f..d7f012288c 100644 --- a/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts +++ b/packages/nodes-base/nodes/Google/CloudStorage/BucketDescription.ts @@ -235,7 +235,7 @@ export const bucketOperations: INodeProperties[] = [ preSend: [parseJSONBody], }, }, - action: 'Create a new Bucket', + action: 'Update the metadata of a Bucket', }, ], default: 'getAll', From 20fd38f3517f7ef35604ba16abb4d951270b4d50 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 02/14] 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 = { From 3348fbb1547c430ff8707b298640e3461d3f6536 Mon Sep 17 00:00:00 2001 From: Jon Date: Thu, 7 Nov 2024 11:53:05 +0000 Subject: [PATCH 03/14] feat(Oura Node): Update node for v2 api (#11604) --- .../credentials/OuraApi.credentials.ts | 23 ++- .../nodes-base/nodes/Oura/GenericFunctions.ts | 12 +- packages/nodes-base/nodes/Oura/Oura.node.ts | 174 +++++++++++------- .../nodes/Oura/test/apiResponses.ts | 8 + .../nodes/Oura/test/oura.node.test.ts | 76 ++++++++ .../nodes/Oura/test/oura_test_workflow.json | 86 +++++++++ 6 files changed, 299 insertions(+), 80 deletions(-) create mode 100644 packages/nodes-base/nodes/Oura/test/apiResponses.ts create mode 100644 packages/nodes-base/nodes/Oura/test/oura.node.test.ts create mode 100644 packages/nodes-base/nodes/Oura/test/oura_test_workflow.json diff --git a/packages/nodes-base/credentials/OuraApi.credentials.ts b/packages/nodes-base/credentials/OuraApi.credentials.ts index b101fac588..759cb71455 100644 --- a/packages/nodes-base/credentials/OuraApi.credentials.ts +++ b/packages/nodes-base/credentials/OuraApi.credentials.ts @@ -1,4 +1,9 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; export class OuraApi implements ICredentialType { name = 'ouraApi'; @@ -16,4 +21,20 @@ export class OuraApi implements ICredentialType { default: '', }, ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials.accessToken}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://api.ouraring.com', + url: '/v2/usercollection/personal_info', + }, + }; } diff --git a/packages/nodes-base/nodes/Oura/GenericFunctions.ts b/packages/nodes-base/nodes/Oura/GenericFunctions.ts index 9fb9691822..cbc562274d 100644 --- a/packages/nodes-base/nodes/Oura/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Oura/GenericFunctions.ts @@ -4,7 +4,7 @@ import type { IHookFunctions, ILoadOptionsFunctions, JsonObject, - IRequestOptions, + IHttpRequestOptions, IHttpRequestMethods, } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; @@ -18,15 +18,11 @@ export async function ouraApiRequest( uri?: string, option: IDataObject = {}, ) { - const credentials = await this.getCredentials('ouraApi'); - let options: IRequestOptions = { - headers: { - Authorization: `Bearer ${credentials.accessToken}`, - }, + let options: IHttpRequestOptions = { method, qs, body, - uri: uri || `https://api.ouraring.com/v1${resource}`, + url: uri ?? `https://api.ouraring.com/v2${resource}`, json: true, }; @@ -41,7 +37,7 @@ export async function ouraApiRequest( options = Object.assign({}, options, option); try { - return await this.helpers.request(options); + return await this.helpers.httpRequestWithAuthentication.call(this, 'ouraApi', options); } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } diff --git a/packages/nodes-base/nodes/Oura/Oura.node.ts b/packages/nodes-base/nodes/Oura/Oura.node.ts index 17ef612cc4..e96b2c57cb 100644 --- a/packages/nodes-base/nodes/Oura/Oura.node.ts +++ b/packages/nodes-base/nodes/Oura/Oura.node.ts @@ -63,94 +63,126 @@ export class Oura implements INodeType { const length = items.length; let responseData; - const returnData: IDataObject[] = []; + const returnData: INodeExecutionData[] = []; const resource = this.getNodeParameter('resource', 0); const operation = this.getNodeParameter('operation', 0); for (let i = 0; i < length; i++) { - if (resource === 'profile') { - // ********************************************************************* - // profile - // ********************************************************************* + try { + if (resource === 'profile') { + // ********************************************************************* + // profile + // ********************************************************************* - // https://cloud.ouraring.com/docs/personal-info + // https://cloud.ouraring.com/docs/personal-info - if (operation === 'get') { - // ---------------------------------- - // profile: get - // ---------------------------------- + if (operation === 'get') { + // ---------------------------------- + // profile: get + // ---------------------------------- - responseData = await ouraApiRequest.call(this, 'GET', '/userinfo'); - } - } else if (resource === 'summary') { - // ********************************************************************* - // summary - // ********************************************************************* - - // https://cloud.ouraring.com/docs/daily-summaries - - const qs: IDataObject = {}; - - const { start, end } = this.getNodeParameter('filters', i) as { - start: string; - end: string; - }; - - const returnAll = this.getNodeParameter('returnAll', 0); - - if (start) { - qs.start = moment(start).format('YYYY-MM-DD'); - } - - if (end) { - qs.end = moment(end).format('YYYY-MM-DD'); - } - - if (operation === 'getActivity') { - // ---------------------------------- - // profile: getActivity - // ---------------------------------- - - responseData = await ouraApiRequest.call(this, 'GET', '/activity', {}, qs); - responseData = responseData.activity; - - if (!returnAll) { - const limit = this.getNodeParameter('limit', 0); - responseData = responseData.splice(0, limit); + responseData = await ouraApiRequest.call(this, 'GET', '/usercollection/personal_info'); } - } else if (operation === 'getReadiness') { - // ---------------------------------- - // profile: getReadiness - // ---------------------------------- + } else if (resource === 'summary') { + // ********************************************************************* + // summary + // ********************************************************************* - responseData = await ouraApiRequest.call(this, 'GET', '/readiness', {}, qs); - responseData = responseData.readiness; + // https://cloud.ouraring.com/docs/daily-summaries - if (!returnAll) { - const limit = this.getNodeParameter('limit', 0); - responseData = responseData.splice(0, limit); + const qs: IDataObject = {}; + + const { start, end } = this.getNodeParameter('filters', i) as { + start: string; + end: string; + }; + + const returnAll = this.getNodeParameter('returnAll', 0); + + if (start) { + qs.start_date = moment(start).format('YYYY-MM-DD'); } - } else if (operation === 'getSleep') { - // ---------------------------------- - // profile: getSleep - // ---------------------------------- - responseData = await ouraApiRequest.call(this, 'GET', '/sleep', {}, qs); - responseData = responseData.sleep; + if (end) { + qs.end_date = moment(end).format('YYYY-MM-DD'); + } - if (!returnAll) { - const limit = this.getNodeParameter('limit', 0); - responseData = responseData.splice(0, limit); + if (operation === 'getActivity') { + // ---------------------------------- + // profile: getActivity + // ---------------------------------- + + responseData = await ouraApiRequest.call( + this, + 'GET', + '/usercollection/daily_activity', + {}, + qs, + ); + responseData = responseData.data; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0); + responseData = responseData.splice(0, limit); + } + } else if (operation === 'getReadiness') { + // ---------------------------------- + // profile: getReadiness + // ---------------------------------- + + responseData = await ouraApiRequest.call( + this, + 'GET', + '/usercollection/daily_readiness', + {}, + qs, + ); + responseData = responseData.data; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0); + responseData = responseData.splice(0, limit); + } + } else if (operation === 'getSleep') { + // ---------------------------------- + // profile: getSleep + // ---------------------------------- + + responseData = await ouraApiRequest.call( + this, + 'GET', + '/usercollection/daily_sleep', + {}, + qs, + ); + responseData = responseData.data; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', 0); + responseData = responseData.splice(0, limit); + } } } + + const executionData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray(responseData as IDataObject[]), + { itemData: { item: i } }, + ); + + returnData.push(...executionData); + } catch (error) { + if (this.continueOnFail()) { + const executionErrorData = this.helpers.constructExecutionMetaData( + this.helpers.returnJsonArray({ error: error.message }), + { itemData: { item: i } }, + ); + returnData.push(...executionErrorData); + continue; + } + throw error; } - - Array.isArray(responseData) - ? returnData.push(...(responseData as IDataObject[])) - : returnData.push(responseData as IDataObject); } - - return [this.helpers.returnJsonArray(returnData)]; + return [returnData]; } } diff --git a/packages/nodes-base/nodes/Oura/test/apiResponses.ts b/packages/nodes-base/nodes/Oura/test/apiResponses.ts new file mode 100644 index 0000000000..5003d94a7a --- /dev/null +++ b/packages/nodes-base/nodes/Oura/test/apiResponses.ts @@ -0,0 +1,8 @@ +export const profileResponse = { + id: 'some-id', + age: 30, + weight: 168, + height: 80, + biological_sex: 'male', + email: 'nathan@n8n.io', +}; diff --git a/packages/nodes-base/nodes/Oura/test/oura.node.test.ts b/packages/nodes-base/nodes/Oura/test/oura.node.test.ts new file mode 100644 index 0000000000..19c1e8c2b4 --- /dev/null +++ b/packages/nodes-base/nodes/Oura/test/oura.node.test.ts @@ -0,0 +1,76 @@ +import type { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IHttpRequestMethods, + INode, +} from 'n8n-workflow'; +import nock from 'nock'; + +import { setup, equalityTest, workflowToTests, getWorkflowFilenames } from '@test/nodes/Helpers'; + +import { profileResponse } from './apiResponses'; +import { ouraApiRequest } from '../GenericFunctions'; + +const node: INode = { + id: '2cdb46cf-b561-4537-a982-b8d26dd7718b', + name: 'Oura', + type: 'n8n-nodes-base.oura', + typeVersion: 1, + position: [0, 0], + parameters: { + resource: 'profile', + operation: 'get', + }, +}; + +const mockThis = { + helpers: { + httpRequestWithAuthentication: jest + .fn() + .mockResolvedValue({ statusCode: 200, data: profileResponse }), + }, + getNode() { + return node; + }, + getNodeParameter: jest.fn(), +} as unknown as IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions; + +describe('Oura', () => { + describe('ouraApiRequest', () => { + it('should make an authenticated API request to Oura', async () => { + const method: IHttpRequestMethods = 'GET'; + const resource = '/usercollection/personal_info'; + + await ouraApiRequest.call(mockThis, method, resource); + + expect(mockThis.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('ouraApi', { + method: 'GET', + url: 'https://api.ouraring.com/v2/usercollection/personal_info', + json: true, + }); + }); + }); + describe('Run Oura workflow', () => { + const workflows = getWorkflowFilenames(__dirname); + const tests = workflowToTests(workflows); + + beforeAll(() => { + nock.disableNetConnect(); + + nock('https://api.ouraring.com/v2') + .get('/usercollection/personal_info') + .reply(200, profileResponse); + }); + + afterAll(() => { + nock.restore(); + }); + + const nodeTypes = setup(tests); + + for (const testData of tests) { + test(testData.description, async () => await equalityTest(testData, nodeTypes)); + } + }); +}); diff --git a/packages/nodes-base/nodes/Oura/test/oura_test_workflow.json b/packages/nodes-base/nodes/Oura/test/oura_test_workflow.json new file mode 100644 index 0000000000..98ec58473e --- /dev/null +++ b/packages/nodes-base/nodes/Oura/test/oura_test_workflow.json @@ -0,0 +1,86 @@ +{ + "name": "Oura Test Workflow", + "nodes": [ + { + "parameters": {}, + "id": "c1e3b825-a9a8-4def-986b-9108d9441992", + "name": "When clicking ‘Test workflow’", + "type": "n8n-nodes-base.manualTrigger", + "position": [720, 400], + "typeVersion": 1 + }, + { + "parameters": { + "resource": "profile" + }, + "id": "7969bf78-9343-4f81-8f79-dc415a60e168", + "name": "Oura", + "type": "n8n-nodes-base.oura", + "typeVersion": 1, + "position": [940, 400], + "credentials": { + "ouraApi": { + "id": "r083EOdhFatkVvFy", + "name": "Oura account" + } + } + }, + { + "parameters": {}, + "id": "9b97fa0e-51a6-41d3-8a7d-cff0531e5527", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [1140, 400] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "id": "some-id", + "age": 30, + "weight": 168, + "height": 80, + "biological_sex": "male", + "email": "nathan@n8n.io" + } + } + ] + }, + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Oura", + "type": "main", + "index": 0 + } + ] + ] + }, + "Oura": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "bd108f46-f6fc-4c22-8655-ade2f51c4b33", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d" + }, + "id": "SrUileWU90mQeo02", + "tags": [] +} From 0c13ad612da62ebf957d8065cb89733351b85f19 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 12:55:36 +0100 Subject: [PATCH 04/14] ci: Cache saving and restoring should use the same key (no-changelog) --- .github/workflows/release-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 21e33bbcff..9196a7fccb 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -42,7 +42,7 @@ jobs: uses: actions/cache/save@v4.0.0 with: path: ./packages/**/dist - key: ${{ github.sha }}-base:build + key: ${{ github.sha }}-release:build - name: Dry-run publishing run: pnpm publish -r --no-git-checks --dry-run @@ -141,7 +141,7 @@ jobs: uses: actions/cache/restore@v4.0.0 with: path: ./packages/**/dist - key: ${{ github.sha }}:db-tests + key: ${{ github.sha }}-release:build - name: Create a frontend release uses: getsentry/action-release@v1.7.0 From d25ae8e0d918d5f41a0cdcf165aa660c68075c6e Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:22:43 +0200 Subject: [PATCH 05/14] refactor: Rename disabled to enabled in runner config (#11621) --- packages/@n8n/config/src/configs/runners.config.ts | 5 ++--- packages/@n8n/config/test/config.test.ts | 2 +- packages/cli/src/commands/start.ts | 2 +- packages/cli/src/commands/worker.ts | 2 +- .../cli/src/runners/__tests__/task-runner-process.test.ts | 2 +- packages/cli/src/runners/task-runner-module.ts | 2 +- packages/cli/test/integration/commands/worker.cmd.test.ts | 2 +- .../integration/runners/task-runner-module.external.test.ts | 4 ++-- .../integration/runners/task-runner-module.internal.test.ts | 4 ++-- .../cli/test/integration/runners/task-runner-process.test.ts | 2 +- packages/nodes-base/nodes/Code/Code.node.ts | 2 +- 11 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/@n8n/config/src/configs/runners.config.ts b/packages/@n8n/config/src/configs/runners.config.ts index 500c3337e9..5a6969ba6f 100644 --- a/packages/@n8n/config/src/configs/runners.config.ts +++ b/packages/@n8n/config/src/configs/runners.config.ts @@ -10,9 +10,8 @@ export type TaskRunnerMode = 'internal_childprocess' | 'internal_launcher' | 'ex @Config export class TaskRunnersConfig { - // Defaults to true for now - @Env('N8N_RUNNERS_DISABLED') - disabled: boolean = true; + @Env('N8N_RUNNERS_ENABLED') + enabled: boolean = false; // Defaults to true for now @Env('N8N_RUNNERS_MODE') diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 71018dee20..eeb98269de 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -222,7 +222,7 @@ describe('GlobalConfig', () => { }, }, taskRunners: { - disabled: true, + enabled: false, mode: 'internal_childprocess', path: '/runners', authToken: '', diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index bb8b56d32b..42b5df13e6 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -221,7 +221,7 @@ export class Start extends BaseCommand { } const { taskRunners: taskRunnerConfig } = this.globalConfig; - if (!taskRunnerConfig.disabled) { + if (taskRunnerConfig.enabled) { const { TaskRunnerModule } = await import('@/runners/task-runner-module'); const taskRunnerModule = Container.get(TaskRunnerModule); await taskRunnerModule.start(); diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 730c6f6e80..0291a9e416 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -113,7 +113,7 @@ export class Worker extends BaseCommand { ); const { taskRunners: taskRunnerConfig } = this.globalConfig; - if (!taskRunnerConfig.disabled) { + if (taskRunnerConfig.enabled) { const { TaskRunnerModule } = await import('@/runners/task-runner-module'); const taskRunnerModule = Container.get(TaskRunnerModule); await taskRunnerModule.start(); diff --git a/packages/cli/src/runners/__tests__/task-runner-process.test.ts b/packages/cli/src/runners/__tests__/task-runner-process.test.ts index eb04e3ab8e..fbab9ee1e3 100644 --- a/packages/cli/src/runners/__tests__/task-runner-process.test.ts +++ b/packages/cli/src/runners/__tests__/task-runner-process.test.ts @@ -22,7 +22,7 @@ require('child_process').spawn = spawnMock; describe('TaskRunnerProcess', () => { const logger = mockInstance(Logger); const runnerConfig = mockInstance(TaskRunnersConfig); - runnerConfig.disabled = false; + runnerConfig.enabled = true; runnerConfig.mode = 'internal_childprocess'; const authService = mock(); let taskRunnerProcess = new TaskRunnerProcess(logger, runnerConfig, authService); diff --git a/packages/cli/src/runners/task-runner-module.ts b/packages/cli/src/runners/task-runner-module.ts index f1383f1fba..fe476ad341 100644 --- a/packages/cli/src/runners/task-runner-module.ts +++ b/packages/cli/src/runners/task-runner-module.ts @@ -26,7 +26,7 @@ export class TaskRunnerModule { constructor(private readonly runnerConfig: TaskRunnersConfig) {} async start() { - a.ok(!this.runnerConfig.disabled, 'Task runner is disabled'); + a.ok(this.runnerConfig.enabled, 'Task runner is disabled'); await this.loadTaskManager(); await this.loadTaskRunnerServer(); diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index ce3280aa48..e17a8d2279 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -26,7 +26,7 @@ import { mockInstance } from '../../shared/mocking'; config.set('executions.mode', 'queue'); config.set('binaryDataManager.availableModes', 'filesystem'); -Container.get(TaskRunnersConfig).disabled = false; +Container.get(TaskRunnersConfig).enabled = true; mockInstance(LoadNodesAndCredentials); const binaryDataService = mockInstance(BinaryDataService); const externalHooks = mockInstance(ExternalHooks); diff --git a/packages/cli/test/integration/runners/task-runner-module.external.test.ts b/packages/cli/test/integration/runners/task-runner-module.external.test.ts index e8a7e54f1a..4974abfb39 100644 --- a/packages/cli/test/integration/runners/task-runner-module.external.test.ts +++ b/packages/cli/test/integration/runners/task-runner-module.external.test.ts @@ -18,14 +18,14 @@ describe('TaskRunnerModule in external mode', () => { describe('start', () => { it('should throw if the task runner is disabled', async () => { - runnerConfig.disabled = true; + runnerConfig.enabled = false; // Act await expect(module.start()).rejects.toThrow('Task runner is disabled'); }); it('should start the task runner', async () => { - runnerConfig.disabled = false; + runnerConfig.enabled = true; // Act await module.start(); diff --git a/packages/cli/test/integration/runners/task-runner-module.internal.test.ts b/packages/cli/test/integration/runners/task-runner-module.internal.test.ts index 922f7fee4b..444d576e87 100644 --- a/packages/cli/test/integration/runners/task-runner-module.internal.test.ts +++ b/packages/cli/test/integration/runners/task-runner-module.internal.test.ts @@ -18,14 +18,14 @@ describe('TaskRunnerModule in internal_childprocess mode', () => { describe('start', () => { it('should throw if the task runner is disabled', async () => { - runnerConfig.disabled = true; + runnerConfig.enabled = false; // Act await expect(module.start()).rejects.toThrow('Task runner is disabled'); }); it('should start the task runner', async () => { - runnerConfig.disabled = false; + runnerConfig.enabled = true; // Act await module.start(); diff --git a/packages/cli/test/integration/runners/task-runner-process.test.ts b/packages/cli/test/integration/runners/task-runner-process.test.ts index 219fbc8813..8c5289a5c7 100644 --- a/packages/cli/test/integration/runners/task-runner-process.test.ts +++ b/packages/cli/test/integration/runners/task-runner-process.test.ts @@ -10,7 +10,7 @@ import { retryUntil } from '@test-integration/retry-until'; describe('TaskRunnerProcess', () => { const authToken = 'token'; const runnerConfig = Container.get(TaskRunnersConfig); - runnerConfig.disabled = false; + runnerConfig.enabled = true; runnerConfig.mode = 'internal_childprocess'; runnerConfig.authToken = authToken; runnerConfig.port = 0; // Use any port diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index e1230b0786..5f9b537d96 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -108,7 +108,7 @@ export class Code implements INodeType { : 'javaScript'; const codeParameterName = language === 'python' ? 'pythonCode' : 'jsCode'; - if (!runnersConfig.disabled && language === 'javaScript') { + if (runnersConfig.enabled && language === 'javaScript') { const code = this.getNodeParameter(codeParameterName, 0) as string; const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this); From 81921387acda3487a58155de51c0edd8dd16a4cb Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 7 Nov 2024 07:28:27 -0500 Subject: [PATCH 06/14] refactor(editor): Migrate `WorkflowShareModal.ee.vue` to composition API (#11605) --- .../src/components/WorkflowShareModal.ee.vue | 432 ++++++++---------- 1 file changed, 198 insertions(+), 234 deletions(-) diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue index d46c1df3b1..7f234b690e 100644 --- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue +++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue @@ -1,6 +1,5 @@ -