From 92af245d1aab5bfad8618fda69b2405f5206875d Mon Sep 17 00:00:00 2001 From: Dana <152518854+dana-gill@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:59:12 +0100 Subject: [PATCH 01/25] fix(Extract from File Node): Detect file encoding (#12081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- packages/core/package.json | 1 + packages/core/src/NodeExecuteFunctions.ts | 5 + .../node-execution-context/execute-context.ts | 2 + .../execute-single-context.ts | 2 + .../supply-data-context.ts | 2 + .../ExtractFromFile/ExtractFromFile.node.ts | 2 +- .../actions/moveTo.operation.ts | 3 +- .../test/ExtractFromFile.node.test.ts | 6 ++ .../test/workflow.non_utf8_encoding.json | 98 +++++++++++++++++++ packages/workflow/src/Interfaces.ts | 2 + pnpm-lock.yaml | 8 ++ 11 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json diff --git a/packages/core/package.json b/packages/core/package.json index 7fdd5f99af..84cd8f7c8a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "@sentry/node": "catalog:", "aws4": "1.11.0", "axios": "catalog:", + "chardet": "2.0.0", "concat-stream": "2.0.0", "cron": "3.1.7", "fast-glob": "catalog:", diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 578752b3ef..38679513de 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -15,6 +15,7 @@ import type { import { ClientOAuth2 } from '@n8n/client-oauth2'; import type { AxiosError, AxiosHeaders, AxiosRequestConfig, AxiosResponse } from 'axios'; import axios from 'axios'; +import chardet from 'chardet'; import crypto, { createHmac } from 'crypto'; import FileType from 'file-type'; import FormData from 'form-data'; @@ -1050,6 +1051,10 @@ export async function getBinaryDataBuffer( return await Container.get(BinaryDataService).getAsBuffer(binaryData); } +export function detectBinaryEncoding(buffer: Buffer): string { + return chardet.detect(buffer) as string; +} + /** * Store an incoming IBinaryData & related buffer using the configured binary data manager. * diff --git a/packages/core/src/node-execution-context/execute-context.ts b/packages/core/src/node-execution-context/execute-context.ts index 954059d86d..d563881bea 100644 --- a/packages/core/src/node-execution-context/execute-context.ts +++ b/packages/core/src/node-execution-context/execute-context.ts @@ -37,6 +37,7 @@ import { getSSHTunnelFunctions, getFileSystemHelperFunctions, getCheckProcessedHelperFunctions, + detectBinaryEncoding, } from '@/NodeExecuteFunctions'; import { BaseExecuteContext } from './base-execute-context'; @@ -96,6 +97,7 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti assertBinaryData(inputData, node, itemIndex, propertyName, 0), getBinaryDataBuffer: async (itemIndex, propertyName) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, 0), + detectBinaryEncoding: (buffer: Buffer) => detectBinaryEncoding(buffer), }; this.nodeHelpers = { 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 cb46ea9c91..af837a12c5 100644 --- a/packages/core/src/node-execution-context/execute-single-context.ts +++ b/packages/core/src/node-execution-context/execute-single-context.ts @@ -16,6 +16,7 @@ import { ApplicationError, createDeferredPromise, NodeConnectionType } from 'n8n // eslint-disable-next-line import/no-cycle import { assertBinaryData, + detectBinaryEncoding, getBinaryDataBuffer, getBinaryHelperFunctions, getRequestHelperFunctions, @@ -69,6 +70,7 @@ export class ExecuteSingleContext extends BaseExecuteContext implements IExecute assertBinaryData(inputData, node, itemIndex, propertyName, inputIndex), getBinaryDataBuffer: async (propertyName, inputIndex = 0) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, inputIndex), + detectBinaryEncoding: (buffer) => detectBinaryEncoding(buffer), }; } diff --git a/packages/core/src/node-execution-context/supply-data-context.ts b/packages/core/src/node-execution-context/supply-data-context.ts index c3d7f45468..6d8679d75e 100644 --- a/packages/core/src/node-execution-context/supply-data-context.ts +++ b/packages/core/src/node-execution-context/supply-data-context.ts @@ -24,6 +24,7 @@ import { assertBinaryData, constructExecutionMetaData, copyInputItems, + detectBinaryEncoding, getBinaryDataBuffer, getBinaryHelperFunctions, getCheckProcessedHelperFunctions, @@ -87,6 +88,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData assertBinaryData(inputData, node, itemIndex, propertyName, 0), getBinaryDataBuffer: async (itemIndex, propertyName) => await getBinaryDataBuffer(inputData, itemIndex, propertyName, 0), + detectBinaryEncoding: (buffer: Buffer) => detectBinaryEncoding(buffer), returnJsonArray, normalizeItems, diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts index 66f6ca2c9c..c863d84818 100644 --- a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts @@ -6,9 +6,9 @@ import type { } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; -import * as spreadsheet from './actions/spreadsheet.operation'; import * as moveTo from './actions/moveTo.operation'; import * as pdf from './actions/pdf.operation'; +import * as spreadsheet from './actions/spreadsheet.operation'; export class ExtractFromFile implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-class-description-missing-subtitle diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts index e4f36afc06..c8b6c115a6 100644 --- a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts @@ -121,8 +121,9 @@ export async function execute( if (!value) continue; - const encoding = (options.encoding as string) || 'utf8'; const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); + const encoding = + (options.encoding as string) || (this.helpers.detectBinaryEncoding(buffer) as string); if (options.keepSource && options.keepSource !== 'binary') { newItem.json = deepCopy(item.json); diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts new file mode 100644 index 0000000000..aebf28eb73 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/test/ExtractFromFile.node.test.ts @@ -0,0 +1,6 @@ +import { getWorkflowFilenames, testWorkflows } from '@test/nodes/Helpers'; + +describe('ExtractFromFile', () => { + const workflows = getWorkflowFilenames(__dirname); + testWorkflows(workflows); +}); diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json b/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json new file mode 100644 index 0000000000..8462d93b93 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/test/workflow.non_utf8_encoding.json @@ -0,0 +1,98 @@ +{ + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-200, -40], + "id": "64686c0a-64a4-4a33-9e70-038c9d23c25b", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "operation": "text", + "binaryPropertyName": "myfile", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [420, -40], + "id": "aaac18d3-1e99-4c47-9de8-2dc8bf95abd7", + "name": "Extract from File" + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "39b5f05f-85c5-499a-86d6-591d6440f147", + "name": "text", + "value": "Karlovy Vary město lázní Příliš žluťoučký kůň úpěl ďábelské ódy.", + "type": "string" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-20, -40], + "id": "e74c67f1-171a-42a7-be12-b6687935988f", + "name": "Edit Fields" + }, + { + "parameters": { + "operation": "toText", + "sourceProperty": "text", + "binaryPropertyName": "myfile", + "options": { + "encoding": "windows1256" + } + }, + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [180, -40], + "id": "77ddd6d4-1d75-4ad2-8301-e70213d8371e", + "name": "windows1256" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [[]] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "windows1256", + "type": "main", + "index": 0 + } + ] + ] + }, + "windows1256": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 116d392722..c5fa0a938a 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -947,6 +947,7 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn & ): NodeExecutionWithMetadata[]; assertBinaryData(itemIndex: number, propertyName: string): IBinaryData; getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise; + detectBinaryEncoding(buffer: Buffer): string; copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[]; }; @@ -973,6 +974,7 @@ export interface IExecuteSingleFunctions extends BaseExecutionFunctions { BinaryHelperFunctions & { assertBinaryData(propertyName: string, inputIndex?: number): IBinaryData; getBinaryDataBuffer(propertyName: string, inputIndex?: number): Promise; + detectBinaryEncoding(buffer: Buffer): string; }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e21c919dee..105345263c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1130,6 +1130,9 @@ importers: axios: specifier: 'catalog:' version: 1.7.4 + chardet: + specifier: 2.0.0 + version: 2.0.0 concat-stream: specifier: 2.0.0 version: 2.0.0 @@ -6833,6 +6836,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.0.0: + resolution: {integrity: sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -19870,6 +19876,8 @@ snapshots: chardet@0.7.0: {} + chardet@2.0.0: {} + charenc@0.0.2: {} chart.js@4.4.0: From 0f1461f2d5d7ec34236ed7fcec3e2f9ee7eb73c4 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:45:05 +0200 Subject: [PATCH 02/25] fix(core): Fix binary data helpers (like `prepareBinaryData`) with task runner (#12259) --- packages/@n8n/task-runner/package.json | 2 + .../__tests__/js-task-runner.test.ts | 122 +++++++++++++++- .../errors/unsupported-function.error.ts | 13 ++ .../src/js-task-runner/js-task-runner.ts | 38 ++++- .../@n8n/task-runner/src/message-types.ts | 6 +- packages/@n8n/task-runner/src/runner-types.ts | 80 ++++++++--- packages/@n8n/task-runner/src/task-runner.ts | 29 +--- .../__tests__/task-manager.test.ts | 136 ++++++++++++++++++ .../src/runners/task-managers/task-manager.ts | 14 +- packages/core/src/SerializedBuffer.ts | 24 ++++ packages/core/src/index.ts | 1 + packages/core/test/SerializedBuffer.test.ts | 55 +++++++ pnpm-lock.yaml | 118 ++++----------- 13 files changed, 495 insertions(+), 143 deletions(-) create mode 100644 packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts create mode 100644 packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts create mode 100644 packages/core/src/SerializedBuffer.ts create mode 100644 packages/core/test/SerializedBuffer.test.ts diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index 4375aa413b..3a5ffc2cf1 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -38,6 +38,7 @@ "@sentry/node": "catalog:", "acorn": "8.14.0", "acorn-walk": "8.3.4", + "lodash.set": "4.3.2", "n8n-core": "workspace:*", "n8n-workflow": "workspace:*", "nanoid": "catalog:", @@ -45,6 +46,7 @@ "ws": "^8.18.0" }, "devDependencies": { + "@types/lodash.set": "4.3.9", "luxon": "catalog:" } } diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index e5df5b64a3..dbb9403894 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -1,5 +1,6 @@ import { mock } from 'jest-mock-extended'; import { DateTime } from 'luxon'; +import type { IBinaryData } from 'n8n-workflow'; import { setGlobalState, type CodeExecutionMode, type IDataObject } from 'n8n-workflow'; import fs from 'node:fs'; import { builtinModules } from 'node:module'; @@ -8,10 +9,15 @@ import type { BaseRunnerConfig } from '@/config/base-runner-config'; import type { JsRunnerConfig } from '@/config/js-runner-config'; import { MainConfig } from '@/config/main-config'; import { ExecutionError } from '@/js-task-runner/errors/execution-error'; +import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error'; import { ValidationError } from '@/js-task-runner/errors/validation-error'; import type { JSExecSettings } from '@/js-task-runner/js-task-runner'; import { JsTaskRunner } from '@/js-task-runner/js-task-runner'; -import type { DataRequestResponse, InputDataChunkDefinition } from '@/runner-types'; +import { + UNSUPPORTED_HELPER_FUNCTIONS, + type DataRequestResponse, + type InputDataChunkDefinition, +} from '@/runner-types'; import type { Task } from '@/task-runner'; import { @@ -567,6 +573,120 @@ describe('JsTaskRunner', () => { ); }); + describe('helpers', () => { + const binaryDataFile: IBinaryData = { + data: 'data', + fileName: 'file.txt', + mimeType: 'text/plain', + }; + + const groups = [ + { + method: 'helpers.assertBinaryData', + invocation: "helpers.assertBinaryData(0, 'binaryFile')", + expectedParams: [0, 'binaryFile'], + }, + { + method: 'helpers.getBinaryDataBuffer', + invocation: "helpers.getBinaryDataBuffer(0, 'binaryFile')", + expectedParams: [0, 'binaryFile'], + }, + { + method: 'helpers.prepareBinaryData', + invocation: "helpers.prepareBinaryData(Buffer.from('123'), 'file.txt', 'text/plain')", + expectedParams: [Buffer.from('123'), 'file.txt', 'text/plain'], + }, + { + method: 'helpers.setBinaryDataBuffer', + invocation: + "helpers.setBinaryDataBuffer({ data: '123', mimeType: 'text/plain' }, Buffer.from('321'))", + expectedParams: [{ data: '123', mimeType: 'text/plain' }, Buffer.from('321')], + }, + { + method: 'helpers.binaryToString', + invocation: "helpers.binaryToString(Buffer.from('123'), 'utf8')", + expectedParams: [Buffer.from('123'), 'utf8'], + }, + { + method: 'helpers.httpRequest', + invocation: "helpers.httpRequest({ method: 'GET', url: 'http://localhost' })", + expectedParams: [{ method: 'GET', url: 'http://localhost' }], + }, + ]; + + for (const group of groups) { + it(`${group.method} for runOnceForAllItems`, async () => { + // Arrange + const rpcCallSpy = jest + .spyOn(defaultTaskRunner, 'makeRpcCall') + .mockResolvedValue(undefined); + + // Act + await execTaskWithParams({ + task: newTaskWithSettings({ + code: `await ${group.invocation}; return []`, + nodeMode: 'runOnceForAllItems', + }), + taskData: newDataRequestResponse( + [{ json: {}, binary: { binaryFile: binaryDataFile } }], + {}, + ), + }); + + expect(rpcCallSpy).toHaveBeenCalledWith('1', group.method, group.expectedParams); + }); + + it(`${group.method} for runOnceForEachItem`, async () => { + // Arrange + const rpcCallSpy = jest + .spyOn(defaultTaskRunner, 'makeRpcCall') + .mockResolvedValue(undefined); + + // Act + await execTaskWithParams({ + task: newTaskWithSettings({ + code: `await ${group.invocation}; return {}`, + nodeMode: 'runOnceForEachItem', + }), + taskData: newDataRequestResponse( + [{ json: {}, binary: { binaryFile: binaryDataFile } }], + {}, + ), + }); + + expect(rpcCallSpy).toHaveBeenCalledWith('1', group.method, group.expectedParams); + }); + } + + describe('unsupported methods', () => { + for (const unsupportedFunction of UNSUPPORTED_HELPER_FUNCTIONS) { + it(`should throw an error if ${unsupportedFunction} is used in runOnceForAllItems`, async () => { + // Act + + await expect( + async () => + await executeForAllItems({ + code: `${unsupportedFunction}()`, + inputItems, + }), + ).rejects.toThrow(UnsupportedFunctionError); + }); + + it(`should throw an error if ${unsupportedFunction} is used in runOnceForEachItem`, async () => { + // Act + + await expect( + async () => + await executeForEachItem({ + code: `${unsupportedFunction}()`, + inputItems, + }), + ).rejects.toThrow(UnsupportedFunctionError); + }); + } + }); + }); + it('should allow access to Node.js Buffers', async () => { const outcomeAll = await execTaskWithParams({ task: newTaskWithSettings({ diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts new file mode 100644 index 0000000000..ad55ee0bbf --- /dev/null +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/unsupported-function.error.ts @@ -0,0 +1,13 @@ +import { ApplicationError } from 'n8n-workflow'; + +/** + * Error that indicates that a specific function is not available in the + * Code Node. + */ +export class UnsupportedFunctionError extends ApplicationError { + constructor(functionName: string) { + super(`The function "${functionName}" is not supported in the Code Node`, { + level: 'info', + }); + } +} diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 89931ce67f..0b3c0a6476 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -1,3 +1,4 @@ +import set from 'lodash.set'; import { getAdditionalKeys } from 'n8n-core'; import { WorkflowDataProxy, Workflow, ObservableObject } from 'n8n-workflow'; import type { @@ -19,11 +20,14 @@ import * as a from 'node:assert'; import { runInNewContext, type Context } from 'node:vm'; import type { MainConfig } from '@/config/main-config'; -import type { - DataRequestResponse, - InputDataChunkDefinition, - PartialAdditionalData, - TaskResultData, +import { UnsupportedFunctionError } from '@/js-task-runner/errors/unsupported-function.error'; +import { + EXPOSED_RPC_METHODS, + UNSUPPORTED_HELPER_FUNCTIONS, + type DataRequestResponse, + type InputDataChunkDefinition, + type PartialAdditionalData, + type TaskResultData, } from '@/runner-types'; import { type Task, TaskRunner } from '@/task-runner'; @@ -38,6 +42,10 @@ import { createRequireResolver } from './require-resolver'; import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation'; import { DataRequestResponseReconstruct } from '../data-request/data-request-response-reconstruct'; +export interface RPCCallObject { + [name: string]: ((...args: unknown[]) => Promise) | RPCCallObject; +} + export interface JSExecSettings { code: string; nodeMode: CodeExecutionMode; @@ -439,4 +447,24 @@ export class JsTaskRunner extends TaskRunner { this.nodeTypes.addNodeTypeDescriptions(nodeTypes); } } + + private buildRpcCallObject(taskId: string) { + const rpcObject: RPCCallObject = {}; + + for (const rpcMethod of EXPOSED_RPC_METHODS) { + set( + rpcObject, + rpcMethod.split('.'), + async (...args: unknown[]) => await this.makeRpcCall(taskId, rpcMethod, args), + ); + } + + for (const rpcMethod of UNSUPPORTED_HELPER_FUNCTIONS) { + set(rpcObject, rpcMethod.split('.'), () => { + throw new UnsupportedFunctionError(rpcMethod); + }); + } + + return rpcObject; + } } diff --git a/packages/@n8n/task-runner/src/message-types.ts b/packages/@n8n/task-runner/src/message-types.ts index 71f236b52a..40f7aeca77 100644 --- a/packages/@n8n/task-runner/src/message-types.ts +++ b/packages/@n8n/task-runner/src/message-types.ts @@ -2,7 +2,7 @@ import type { INodeTypeBaseDescription } from 'n8n-workflow'; import type { NeededNodeType, - RPC_ALLOW_LIST, + AVAILABLE_RPC_METHODS, TaskDataRequestParams, TaskResultData, } from './runner-types'; @@ -105,7 +105,7 @@ export namespace BrokerMessage { type: 'broker:rpc'; callId: string; taskId: string; - name: (typeof RPC_ALLOW_LIST)[number]; + name: (typeof AVAILABLE_RPC_METHODS)[number]; params: unknown[]; } @@ -239,7 +239,7 @@ export namespace RunnerMessage { type: 'runner:rpc'; callId: string; taskId: string; - name: (typeof RPC_ALLOW_LIST)[number]; + name: (typeof AVAILABLE_RPC_METHODS)[number]; params: unknown[]; } diff --git a/packages/@n8n/task-runner/src/runner-types.ts b/packages/@n8n/task-runner/src/runner-types.ts index 5075b19db2..e4e76189e2 100644 --- a/packages/@n8n/task-runner/src/runner-types.ts +++ b/packages/@n8n/task-runner/src/runner-types.ts @@ -100,31 +100,73 @@ export interface PartialAdditionalData { variables: IDataObject; } -export const RPC_ALLOW_LIST = [ +/** RPC methods that are exposed directly to the Code Node */ +export const EXPOSED_RPC_METHODS = [ + // assertBinaryData(itemIndex: number, propertyName: string): Promise + 'helpers.assertBinaryData', + + // getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise + 'helpers.getBinaryDataBuffer', + + // prepareBinaryData(binaryData: Buffer, fileName?: string, mimeType?: string): Promise + 'helpers.prepareBinaryData', + + // setBinaryDataBuffer(metadata: IBinaryData, buffer: Buffer): Promise + 'helpers.setBinaryDataBuffer', + + // binaryToString(body: Buffer, encoding?: string): string + 'helpers.binaryToString', + + // httpRequest(opts: IHttpRequestOptions): Promise + 'helpers.httpRequest', +]; + +/** Helpers that exist but that we are not exposing to the Code Node */ +export const UNSUPPORTED_HELPER_FUNCTIONS = [ + // These rely on checking the credentials from the current node type (Code Node) + // and hence they can't even work (Code Node doesn't have credentials) 'helpers.httpRequestWithAuthentication', 'helpers.requestWithAuthenticationPaginated', - // "helpers.normalizeItems" - // "helpers.constructExecutionMetaData" - // "helpers.assertBinaryData" - 'helpers.getBinaryDataBuffer', - // "helpers.copyInputItems" - // "helpers.returnJsonArray" - 'helpers.getSSHClient', - 'helpers.createReadStream', - // "helpers.getStoragePath" - 'helpers.writeContentToFile', - 'helpers.prepareBinaryData', - 'helpers.setBinaryDataBuffer', + + // This has been removed 'helpers.copyBinaryFile', - 'helpers.binaryToBuffer', - // "helpers.binaryToString" - // "helpers.getBinaryPath" + + // We can't support streams over RPC without implementing it ourselves + 'helpers.createReadStream', 'helpers.getBinaryStream', + + // Makes no sense to support this, as it returns either a stream or a buffer + // and we can't support streams over RPC + 'helpers.binaryToBuffer', + + // These are pretty low-level, so we shouldn't expose them + // (require binary data id, which we don't expose) 'helpers.getBinaryMetadata', + 'helpers.getStoragePath', + 'helpers.getBinaryPath', + + // We shouldn't allow arbitrary FS writes + 'helpers.writeContentToFile', + + // Not something we need to expose. Can be done in the node itself + // copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[] + 'helpers.copyInputItems', + + // Code Node does these automatically already + 'helpers.returnJsonArray', + 'helpers.normalizeItems', + + // The client is instantiated and lives on the n8n instance, so we can't + // expose it over RPC without implementing object marshalling + 'helpers.getSSHClient', + + // Doesn't make sense to expose 'helpers.createDeferredPromise', - 'helpers.httpRequest', - 'logNodeOutput', -] as const; + 'helpers.constructExecutionMetaData', +]; + +/** List of all RPC methods that task runner supports */ +export const AVAILABLE_RPC_METHODS = [...EXPOSED_RPC_METHODS, 'logNodeOutput'] as const; /** Node types needed for the runner to execute a task. */ export type NeededNodeType = { name: string; version: number }; diff --git a/packages/@n8n/task-runner/src/task-runner.ts b/packages/@n8n/task-runner/src/task-runner.ts index e8ee605ef5..4254aad99c 100644 --- a/packages/@n8n/task-runner/src/task-runner.ts +++ b/packages/@n8n/task-runner/src/task-runner.ts @@ -1,3 +1,4 @@ +import { isSerializedBuffer, toBuffer } from 'n8n-core'; import { ApplicationError, ensureError, randomInt } from 'n8n-workflow'; import { nanoid } from 'nanoid'; import { EventEmitter } from 'node:events'; @@ -6,7 +7,7 @@ import { type MessageEvent, WebSocket } from 'ws'; import type { BaseRunnerConfig } from '@/config/base-runner-config'; import type { BrokerMessage, RunnerMessage } from '@/message-types'; import { TaskRunnerNodeTypes } from '@/node-types'; -import { RPC_ALLOW_LIST, type TaskResultData } from '@/runner-types'; +import type { TaskResultData } from '@/runner-types'; import { TaskCancelledError } from './js-task-runner/errors/task-cancelled-error'; @@ -42,10 +43,6 @@ interface RPCCall { reject: (error: unknown) => void; } -export interface RPCCallObject { - [name: string]: ((...args: unknown[]) => Promise) | RPCCallObject; -} - const OFFER_VALID_TIME_MS = 5000; const OFFER_VALID_EXTRA_MS = 100; @@ -464,7 +461,9 @@ export abstract class TaskRunner extends EventEmitter { }); try { - return await dataPromise; + const returnValue = await dataPromise; + + return isSerializedBuffer(returnValue) ? toBuffer(returnValue) : returnValue; } finally { this.rpcCalls.delete(callId); } @@ -486,24 +485,6 @@ export abstract class TaskRunner extends EventEmitter { } } - buildRpcCallObject(taskId: string) { - const rpcObject: RPCCallObject = {}; - for (const r of RPC_ALLOW_LIST) { - const splitPath = r.split('.'); - let obj = rpcObject; - - splitPath.forEach((s, index) => { - if (index !== splitPath.length - 1) { - obj[s] = {}; - obj = obj[s]; - return; - } - obj[s] = async (...args: unknown[]) => await this.makeRpcCall(taskId, r, args); - }); - } - return rpcObject; - } - /** Close the connection gracefully and wait until has been closed */ async stop() { this.clearIdleTimer(); diff --git a/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts b/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts new file mode 100644 index 0000000000..84584e05df --- /dev/null +++ b/packages/cli/src/runners/task-managers/__tests__/task-manager.test.ts @@ -0,0 +1,136 @@ +import { mock } from 'jest-mock-extended'; +import { get, set } from 'lodash'; + +import type { NodeTypes } from '@/node-types'; +import type { Task } from '@/runners/task-managers/task-manager'; +import { TaskManager } from '@/runners/task-managers/task-manager'; + +class TestTaskManager extends TaskManager { + sentMessages: unknown[] = []; + + sendMessage(message: unknown) { + this.sentMessages.push(message); + } +} + +describe('TaskManager', () => { + let instance: TestTaskManager; + const mockNodeTypes = mock(); + + beforeEach(() => { + instance = new TestTaskManager(mockNodeTypes); + }); + + describe('handleRpc', () => { + test.each([ + ['logNodeOutput', ['hello world']], + ['helpers.assertBinaryData', [0, 'propertyName']], + ['helpers.getBinaryDataBuffer', [0, 'propertyName']], + ['helpers.prepareBinaryData', [Buffer.from('data').toJSON(), 'filename', 'mimetype']], + ['helpers.setBinaryDataBuffer', [{ data: '123' }, Buffer.from('data').toJSON()]], + ['helpers.binaryToString', [Buffer.from('data').toJSON(), 'utf8']], + ['helpers.httpRequest', [{ url: 'http://localhost' }]], + ])('should handle %s rpc call', async (methodName, args) => { + const executeFunctions = set({}, methodName.split('.'), jest.fn()); + + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', methodName, args); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: undefined, + status: 'success', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + expect(get(executeFunctions, methodName.split('.'))).toHaveBeenCalledWith(...args); + }); + + it('converts any serialized buffer arguments into buffers', async () => { + const mockPrepareBinaryData = jest.fn().mockResolvedValue(undefined); + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: { + helpers: { + prepareBinaryData: mockPrepareBinaryData, + }, + }, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'helpers.prepareBinaryData', [ + Buffer.from('data').toJSON(), + 'filename', + 'mimetype', + ]); + + expect(mockPrepareBinaryData).toHaveBeenCalledWith( + Buffer.from('data'), + 'filename', + 'mimetype', + ); + }); + + describe('errors', () => { + it('sends method not allowed error if method is not in the allow list', async () => { + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: {}, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'notAllowedMethod', []); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: 'Method not allowed', + status: 'error', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + }); + + it('sends error if method throws', async () => { + const error = new Error('Test error'); + const mockTask = mock({ + taskId: 'taskId', + data: { + executeFunctions: { + helpers: { + assertBinaryData: jest.fn().mockRejectedValue(error), + }, + }, + }, + }); + instance.tasks.set('taskId', mockTask); + + await instance.handleRpc('taskId', 'callId', 'helpers.assertBinaryData', []); + + expect(instance.sentMessages).toEqual([ + { + callId: 'callId', + data: error, + status: 'error', + taskId: 'taskId', + type: 'requester:rpcresponse', + }, + ]); + }); + }); + }); +}); diff --git a/packages/cli/src/runners/task-managers/task-manager.ts b/packages/cli/src/runners/task-managers/task-manager.ts index fd62dc2673..44193f9377 100644 --- a/packages/cli/src/runners/task-managers/task-manager.ts +++ b/packages/cli/src/runners/task-managers/task-manager.ts @@ -1,5 +1,6 @@ import type { TaskResultData, RequesterMessage, BrokerMessage, TaskData } from '@n8n/task-runner'; -import { RPC_ALLOW_LIST } from '@n8n/task-runner'; +import { AVAILABLE_RPC_METHODS } from '@n8n/task-runner'; +import { isSerializedBuffer, toBuffer } from 'n8n-core'; import { createResultOk, createResultError } from 'n8n-workflow'; import type { EnvProviderState, @@ -288,7 +289,7 @@ export abstract class TaskManager { } try { - if (!RPC_ALLOW_LIST.includes(name)) { + if (!AVAILABLE_RPC_METHODS.includes(name)) { this.sendMessage({ type: 'requester:rpcresponse', taskId, @@ -322,6 +323,15 @@ export abstract class TaskManager { }); return; } + + // Convert any serialized buffers back to buffers + for (let i = 0; i < params.length; i++) { + const paramValue = params[i]; + if (isSerializedBuffer(paramValue)) { + params[i] = toBuffer(paramValue); + } + } + const data = (await func.call(funcs, ...params)) as unknown; this.sendMessage({ diff --git a/packages/core/src/SerializedBuffer.ts b/packages/core/src/SerializedBuffer.ts new file mode 100644 index 0000000000..48395049b9 --- /dev/null +++ b/packages/core/src/SerializedBuffer.ts @@ -0,0 +1,24 @@ +/** A nodejs Buffer gone through JSON.stringify */ +export type SerializedBuffer = { + type: 'Buffer'; + data: number[]; // Array like Uint8Array, each item is uint8 (0-255) +}; + +/** Converts the given SerializedBuffer to nodejs Buffer */ +export function toBuffer(serializedBuffer: SerializedBuffer): Buffer { + return Buffer.from(serializedBuffer.data); +} + +function isObjectLiteral(item: unknown): item is { [key: string]: unknown } { + return typeof item === 'object' && item !== null && !Array.isArray(item); +} + +export function isSerializedBuffer(candidate: unknown): candidate is SerializedBuffer { + return ( + isObjectLiteral(candidate) && + 'type' in candidate && + 'data' in candidate && + candidate.type === 'Buffer' && + Array.isArray(candidate.data) + ); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f2f2149b60..1fc9d77399 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,3 +24,4 @@ export * from './ExecutionMetadata'; export * from './node-execution-context'; export * from './PartialExecutionUtils'; export { ErrorReporter } from './error-reporter'; +export * from './SerializedBuffer'; diff --git a/packages/core/test/SerializedBuffer.test.ts b/packages/core/test/SerializedBuffer.test.ts new file mode 100644 index 0000000000..4243729629 --- /dev/null +++ b/packages/core/test/SerializedBuffer.test.ts @@ -0,0 +1,55 @@ +import type { SerializedBuffer } from '@/SerializedBuffer'; +import { toBuffer, isSerializedBuffer } from '@/SerializedBuffer'; + +// Mock data for tests +const validSerializedBuffer: SerializedBuffer = { + type: 'Buffer', + data: [65, 66, 67], // Corresponds to 'ABC' in ASCII +}; + +describe('serializedBufferToBuffer', () => { + it('should convert a SerializedBuffer to a Buffer', () => { + const buffer = toBuffer(validSerializedBuffer); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.toString()).toBe('ABC'); + }); + + it('should serialize stringified buffer to the same buffer', () => { + const serializedBuffer = JSON.stringify(Buffer.from('n8n on the rocks')); + const buffer = toBuffer(JSON.parse(serializedBuffer)); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.toString()).toBe('n8n on the rocks'); + }); +}); + +describe('isSerializedBuffer', () => { + it('should return true for a valid SerializedBuffer', () => { + expect(isSerializedBuffer(validSerializedBuffer)).toBe(true); + }); + + test.each([ + [{ data: [1, 2, 3] }], + [{ data: [1, 2, 256] }], + [{ type: 'Buffer', data: 'notAnArray' }], + [{ data: 42 }], + [{ data: 'test' }], + [{ data: true }], + [null], + [undefined], + [42], + [{}], + ])('should return false for %s', (value) => { + expect(isSerializedBuffer(value)).toBe(false); + }); +}); + +describe('Integration: serializedBufferToBuffer and isSerializedBuffer', () => { + it('should correctly validate and convert a SerializedBuffer', () => { + if (isSerializedBuffer(validSerializedBuffer)) { + const buffer = toBuffer(validSerializedBuffer); + expect(buffer.toString()).toBe('ABC'); + } else { + fail('Expected validSerializedBuffer to be a SerializedBuffer'); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 105345263c..164e9af759 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,6 +672,9 @@ importers: acorn-walk: specifier: 8.3.4 version: 8.3.4 + lodash.set: + specifier: 4.3.2 + version: 4.3.2 n8n-core: specifier: workspace:* version: link:../../core @@ -688,6 +691,9 @@ importers: specifier: '>=8.17.1' version: 8.17.1 devDependencies: + '@types/lodash.set': + specifier: 4.3.9 + version: 4.3.9 luxon: specifier: 'catalog:' version: 3.4.4 @@ -1114,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1966,7 +1972,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1) + version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -5621,6 +5627,9 @@ packages: '@types/lodash-es@4.17.6': resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} + '@types/lodash.set@4.3.9': + resolution: {integrity: sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==} + '@types/lodash@4.14.195': resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} @@ -9736,6 +9745,9 @@ packages: lodash.orderby@4.6.0: resolution: {integrity: sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==} + lodash.set@4.3.2: + resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -16206,38 +16218,6 @@ snapshots: transitivePeerDependencies: - openai - '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - - '@langchain/core@0.3.19(openai@4.73.1)': - dependencies: - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 - js-tiktoken: 1.0.12 - langsmith: 0.2.3(openai@4.73.1) - mustache: 4.2.0 - p-queue: 6.6.2 - p-retry: 4.6.2 - uuid: 10.0.0 - zod: 3.23.8 - zod-to-json-schema: 3.23.3(zod@3.23.8) - transitivePeerDependencies: - - openai - '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -18361,6 +18341,10 @@ snapshots: dependencies: '@types/lodash': 4.14.195 + '@types/lodash.set@4.3.9': + dependencies: + '@types/lodash': 4.14.195 + '@types/lodash@4.14.195': {} '@types/long@4.0.2': {} @@ -19466,14 +19450,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.7: - dependencies: - follow-redirects: 1.15.6(debug@4.3.6) - form-data: 4.0.0 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.7(debug@4.3.6): dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21187,7 +21163,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21212,7 +21188,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21232,7 +21208,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -22011,7 +21987,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -22392,7 +22368,7 @@ snapshots: infisical-node@1.3.0: dependencies: - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) dotenv: 16.3.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -23369,28 +23345,6 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) - langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - - langsmith@0.2.3(openai@4.73.1): - dependencies: - '@types/uuid': 10.0.0 - commander: 10.0.1 - p-queue: 6.6.2 - p-retry: 4.6.2 - semver: 7.6.0 - uuid: 10.0.0 - optionalDependencies: - openai: 4.73.1(zod@3.23.8) - lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -23572,6 +23526,8 @@ snapshots: lodash.orderby@4.6.0: {} + lodash.set@4.3.2: {} + lodash.throttle@4.1.1: {} lodash@4.17.21: {} @@ -24723,22 +24679,6 @@ snapshots: - encoding - supports-color - openai@4.73.1(zod@3.23.8): - dependencies: - '@types/node': 18.16.16 - '@types/node-fetch': 2.6.4 - abort-controller: 3.0.0 - agentkeepalive: 4.2.1 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0(encoding@0.1.13) - optionalDependencies: - zod: 3.23.8 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -24919,7 +24859,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25121,7 +25061,7 @@ snapshots: posthog-node@3.2.1: dependencies: - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) rusha: 0.8.14 transitivePeerDependencies: - debug @@ -25761,7 +25701,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@5.5.0) + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -26139,7 +26079,7 @@ snapshots: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - axios: 1.7.7 + axios: 1.7.7(debug@4.3.6) big-integer: 1.6.51 bignumber.js: 9.1.2 binascii: 0.0.2 From dc7864a86d13a4a9cf0e72839e0e1945edb7c7a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 18 Dec 2024 20:05:00 +0100 Subject: [PATCH 03/25] docs(core): Update pruning service docline (#12282) --- .../@n8n/config/src/configs/executions.config.ts | 2 +- .../cli/src/services/pruning/pruning.service.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/@n8n/config/src/configs/executions.config.ts b/packages/@n8n/config/src/configs/executions.config.ts index 8c5d91b3c8..977d539920 100644 --- a/packages/@n8n/config/src/configs/executions.config.ts +++ b/packages/@n8n/config/src/configs/executions.config.ts @@ -6,7 +6,7 @@ class PruningIntervalsConfig { @Env('EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL') hardDelete: number = 15; - /** How often (minutes) execution data should be soft-deleted */ + /** How often (minutes) execution data should be soft-deleted. */ @Env('EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL') softDelete: number = 60; } diff --git a/packages/cli/src/services/pruning/pruning.service.ts b/packages/cli/src/services/pruning/pruning.service.ts index a7bc56725d..aad8c5490f 100644 --- a/packages/cli/src/services/pruning/pruning.service.ts +++ b/packages/cli/src/services/pruning/pruning.service.ts @@ -13,9 +13,17 @@ import { Logger } from '@/logging/logger.service'; import { OrchestrationService } from '../orchestration.service'; /** - * Responsible for pruning executions from the database and their associated binary data - * from the filesystem, on a rolling basis. By default we soft-delete execution rows - * every cycle and hard-delete them and their binary data every 4th cycle. + * Responsible for deleting old executions from the database and deleting their + * associated binary data from the filesystem, on a rolling basis. + * + * By default: + * + * - Soft deletion (every 60m) identifies all prunable executions based on max + * age and/or max count, exempting annotated executions. + * - Hard deletion (every 15m) processes prunable executions in batches of 100, + * switching to 1s intervals until the total to prune is back down low enough, + * or in case the hard deletion fails. + * - Once mostly caught up, hard deletion goes back to the 15m schedule. */ @Service() export class PruningService { From 7ce4e8d169d82b31f8e62af53d386415b5729090 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: Wed, 18 Dec 2024 20:05:41 +0100 Subject: [PATCH 04/25] fix(core): Switch from `lodash.set` to `lodash` to address CVE-2020-8203 (no-changelog) (#12286) --- packages/@n8n/task-runner/package.json | 4 +- .../src/js-task-runner/js-task-runner.ts | 2 +- pnpm-lock.yaml | 124 ++++++++++++++---- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index 3a5ffc2cf1..f82692db77 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -38,7 +38,7 @@ "@sentry/node": "catalog:", "acorn": "8.14.0", "acorn-walk": "8.3.4", - "lodash.set": "4.3.2", + "lodash": "catalog:", "n8n-core": "workspace:*", "n8n-workflow": "workspace:*", "nanoid": "catalog:", @@ -46,7 +46,7 @@ "ws": "^8.18.0" }, "devDependencies": { - "@types/lodash.set": "4.3.9", + "@types/lodash": "catalog:", "luxon": "catalog:" } } diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index 0b3c0a6476..04e05fb30a 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -1,4 +1,4 @@ -import set from 'lodash.set'; +import set from 'lodash/set'; import { getAdditionalKeys } from 'n8n-core'; import { WorkflowDataProxy, Workflow, ObservableObject } from 'n8n-workflow'; import type { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 164e9af759..5744a83fb2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -672,9 +672,9 @@ importers: acorn-walk: specifier: 8.3.4 version: 8.3.4 - lodash.set: - specifier: 4.3.2 - version: 4.3.2 + lodash: + specifier: 'catalog:' + version: 4.17.21 n8n-core: specifier: workspace:* version: link:../../core @@ -691,9 +691,9 @@ importers: specifier: '>=8.17.1' version: 8.17.1 devDependencies: - '@types/lodash.set': - specifier: 4.3.9 - version: 4.3.9 + '@types/lodash': + specifier: 'catalog:' + version: 4.14.195 luxon: specifier: 'catalog:' version: 3.4.4 @@ -1120,7 +1120,7 @@ importers: dependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + version: 0.3.19(openai@4.73.1(zod@3.23.8)) '@n8n/client-oauth2': specifier: workspace:* version: link:../@n8n/client-oauth2 @@ -1972,7 +1972,7 @@ importers: devDependencies: '@langchain/core': specifier: 'catalog:' - version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) + version: 0.3.19(openai@4.73.1) '@types/deep-equal': specifier: ^1.0.1 version: 1.0.1 @@ -5627,9 +5627,6 @@ packages: '@types/lodash-es@4.17.6': resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} - '@types/lodash.set@4.3.9': - resolution: {integrity: sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==} - '@types/lodash@4.14.195': resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} @@ -9745,9 +9742,6 @@ packages: lodash.orderby@4.6.0: resolution: {integrity: sha512-T0rZxKmghOOf5YPnn8EY5iLYeWCpZq8G41FfqoVHH5QDTAFaghJRmAdLiadEDq+ztgM2q5PjA+Z1fOwGrLgmtg==} - lodash.set@4.3.2: - resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} - lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -16218,6 +16212,38 @@ snapshots: transitivePeerDependencies: - openai + '@langchain/core@0.3.19(openai@4.73.1(zod@3.23.8))': + dependencies: + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.2.3(openai@4.73.1(zod@3.23.8)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.23.8 + zod-to-json-schema: 3.23.3(zod@3.23.8) + transitivePeerDependencies: + - openai + + '@langchain/core@0.3.19(openai@4.73.1)': + dependencies: + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.12 + langsmith: 0.2.3(openai@4.73.1) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.23.8 + zod-to-json-schema: 3.23.3(zod@3.23.8) + transitivePeerDependencies: + - openai + '@langchain/google-common@0.1.3(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)': dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -18341,10 +18367,6 @@ snapshots: dependencies: '@types/lodash': 4.14.195 - '@types/lodash.set@4.3.9': - dependencies: - '@types/lodash': 4.14.195 - '@types/lodash@4.14.195': {} '@types/long@4.0.2': {} @@ -19450,6 +19472,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.7: + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.7(debug@4.3.6): dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21163,7 +21193,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21188,7 +21218,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21208,7 +21238,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -21987,7 +22017,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22368,7 +22398,7 @@ snapshots: infisical-node@1.3.0: dependencies: - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 dotenv: 16.3.1 tweetnacl: 1.0.3 tweetnacl-util: 0.15.1 @@ -23345,6 +23375,28 @@ snapshots: optionalDependencies: openai: 4.73.1(encoding@0.1.13)(zod@3.23.8) + langsmith@0.2.3(openai@4.73.1(zod@3.23.8)): + dependencies: + '@types/uuid': 10.0.0 + commander: 10.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.6.0 + uuid: 10.0.0 + optionalDependencies: + openai: 4.73.1(zod@3.23.8) + + langsmith@0.2.3(openai@4.73.1): + dependencies: + '@types/uuid': 10.0.0 + commander: 10.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + semver: 7.6.0 + uuid: 10.0.0 + optionalDependencies: + openai: 4.73.1(zod@3.23.8) + lazy-ass@1.6.0: {} ldapts@4.2.6: @@ -23526,8 +23578,6 @@ snapshots: lodash.orderby@4.6.0: {} - lodash.set@4.3.2: {} - lodash.throttle@4.1.1: {} lodash@4.17.21: {} @@ -24679,6 +24729,22 @@ snapshots: - encoding - supports-color + openai@4.73.1(zod@3.23.8): + dependencies: + '@types/node': 18.16.16 + '@types/node-fetch': 2.6.4 + abort-controller: 3.0.0 + agentkeepalive: 4.2.1 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + zod: 3.23.8 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + openapi-sampler@1.5.1: dependencies: '@types/json-schema': 7.0.15 @@ -24859,7 +24925,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25061,7 +25127,7 @@ snapshots: posthog-node@3.2.1: dependencies: - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 rusha: 0.8.14 transitivePeerDependencies: - debug @@ -25701,7 +25767,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -26079,7 +26145,7 @@ snapshots: asn1.js: 5.4.1 asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1) asn1.js-rfc5280: 3.0.0 - axios: 1.7.7(debug@4.3.6) + axios: 1.7.7 big-integer: 1.6.51 bignumber.js: 9.1.2 binascii: 0.0.2 From 6e44c71c9ca82cce20eb55bb9003930bbf66a16c Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:39:10 +0000 Subject: [PATCH 05/25] feat(editor): Params pane collection improvements (#11607) Co-authored-by: Elias Meire --- cypress/e2e/16-form-trigger-node.cy.ts | 35 ++-- cypress/e2e/5-ndv.cy.ts | 20 -- .../design-system/src/css/_tokens.dark.scss | 3 + packages/design-system/src/css/_tokens.scss | 4 + packages/editor-ui/package.json | 1 + .../AssignmentCollection/Assignment.vue | 23 ++- .../AssignmentCollection.vue | 49 +++-- .../components/FilterConditions/Condition.vue | 27 ++- .../FilterConditions/FilterConditions.vue | 75 ++++--- .../components/FixedCollectionParameter.vue | 185 +++++++++--------- .../src/components/ParameterInputList.vue | 36 ++-- .../src/plugins/i18n/locales/en.json | 2 + pnpm-lock.yaml | 58 ++++-- 13 files changed, 310 insertions(+), 208 deletions(-) diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts index 60fbd7c419..ed901107ea 100644 --- a/cypress/e2e/16-form-trigger-node.cy.ts +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -44,8 +44,7 @@ describe('n8n Form Trigger', () => { ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 3') - .blur(); + .type('Test Field 3'); cy.get( ':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); @@ -56,27 +55,24 @@ describe('n8n Form Trigger', () => { ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item', ) .find('input[placeholder*="e.g. What is your name?"]') - .type('Test Field 4') - .blur(); + .type('Test Field 4'); cy.get( ':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item', ).click(); getVisibleSelect().contains('Dropdown').click(); - cy.get( - '.border-top-dashed > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > :nth-child(2) > .button', - ).click(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)', - ) - .find('input') - .type('Option 1') - .blur(); - cy.get( - ':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)', - ) - .find('input') - .type('Option 2') - .blur(); + cy.contains('button', 'Add Field Option').click(); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(0) + .type('Option 1'); + cy.contains('label', 'Field Options') + .parent() + .nextAll() + .find('[data-test-id="parameter-input-field"]') + .eq(1) + .type('Option 2'); //add optional submitted message cy.get('.param-options').click(); @@ -94,7 +90,6 @@ describe('n8n Form Trigger', () => { .children() .children() .first() - .clear() .type('Your test form was successfully submitted'); ndv.getters.backToCanvas().click(); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index e8215db38f..8bad424554 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -65,26 +65,6 @@ describe('NDV', () => { cy.shouldNotHaveConsoleErrors(); }); - it('should disconect Switch outputs if rules order was changed', () => { - cy.createFixtureWorkflow('NDV-test-switch_reorder.json', 'NDV test switch reorder'); - workflowPage.actions.zoomToFit(); - - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('2 items').should('exist'); - cy.contains('span', 'first').should('exist'); - ndv.getters.backToCanvas().click(); - - workflowPage.actions.openNode('Switch'); - cy.get('.cm-line').realMouseMove(100, 100); - cy.get('.fa-angle-down').first().click(); - ndv.getters.backToCanvas().click(); - workflowPage.actions.executeWorkflow(); - workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('2 items').should('exist'); - cy.contains('span', 'zero').should('exist'); - }); - it('should show correct validation state for resource locator params', () => { workflowPage.actions.addNodeToCanvas('Typeform', true, true); ndv.getters.container().should('be.visible'); diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index a3fc653550..4390bc1da8 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -462,6 +462,9 @@ --color-configurable-node-name: var(--color-text-dark); --color-secondary-link: var(--prim-color-secondary-tint-200); --color-secondary-link-hover: var(--prim-color-secondary-tint-100); + //Params + --color-icon-base: var(--color-text-light); + --color-icon-hover: var(--prim-color-primary); --color-menu-background: var(--prim-gray-740); --color-menu-hover-background: var(--prim-gray-670); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index b8f3049cf2..944652e43e 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -621,6 +621,10 @@ --spacing-3xl: 4rem; --spacing-4xl: 8rem; --spacing-5xl: 16rem; + + //Params + --color-icon-base: var(--color-text-light); + --color-icon-hover: var(--prim-color-primary); } :root { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 3cf113d98f..68b13e13f4 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -82,6 +82,7 @@ "vue-router": "catalog:frontend", "vue-virtual-scroller": "2.0.0-beta.8", "vue3-touch-events": "^4.1.3", + "vuedraggable": "4.1.0", "xss": "catalog:" }, "devDependencies": { diff --git a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue index 7240dac1e2..614ae3f590 100644 --- a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue +++ b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue @@ -152,6 +152,14 @@ const onBlur = (): void => { }" data-test-id="assignment" > + { size="mini" icon="trash" data-test-id="assignment-remove" - :class="$style.remove" + :class="[$style.iconButton, $style.extraTopPadding]" @click="onRemove" > @@ -241,7 +249,7 @@ const onBlur = (): void => { } &:hover { - .remove { + .iconButton { opacity: 1; } } @@ -269,12 +277,19 @@ const onBlur = (): void => { } } -.remove { +.iconButton { position: absolute; left: 0; - top: var(--spacing-l); opacity: 0; transition: opacity 100ms ease-in; + color: var(--icon-base-color); +} +.extraTopPadding { + top: calc(20px + var(--spacing-l)); +} + +.defaultTopPadding { + top: var(--spacing-l); } .status { diff --git a/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue b/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue index 24545bd7b9..0aebc16bb6 100644 --- a/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue +++ b/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue @@ -15,6 +15,7 @@ import ParameterOptions from '../ParameterOptions.vue'; import Assignment from './Assignment.vue'; import { inputDataToAssignments, typeFromExpression } from './utils'; import { propertyNameFromExpression } from '@/utils/mappingUtils'; +import Draggable from 'vuedraggable'; interface Props { parameter: INodeProperties; @@ -133,19 +134,27 @@ function optionSelected(action: string) {
-
- - -
+ + +
diff --git a/packages/editor-ui/src/components/FilterConditions/Condition.vue b/packages/editor-ui/src/components/FilterConditions/Condition.vue index 316b6827fa..be4d02224c 100644 --- a/packages/editor-ui/src/components/FilterConditions/Condition.vue +++ b/packages/editor-ui/src/components/FilterConditions/Condition.vue @@ -33,6 +33,7 @@ interface Props { canRemove?: boolean; readOnly?: boolean; index?: number; + canDrag?: boolean; } const props = withDefaults(defineProps(), { @@ -41,6 +42,7 @@ const props = withDefaults(defineProps(), { fixedLeftValue: false, readOnly: false, index: 0, + canDrag: true, }); const emit = defineEmits<{ @@ -152,6 +154,15 @@ const onBlur = (): void => { }" data-test-id="filter-condition" > + { icon="trash" data-test-id="filter-remove-condition" :title="i18n.baseText('filter.removeCondition')" - :class="$style.remove" + :class="[$style.iconButton, $style.extraTopPadding]" @click="onRemove" > @@ -248,7 +259,7 @@ const onBlur = (): void => { } &:hover { - .remove { + .iconButton { opacity: 1; } } @@ -261,13 +272,21 @@ const onBlur = (): void => { .statusIcon { padding-left: var(--spacing-4xs); + padding-right: var(--spacing-4xs); } -.remove { +.iconButton { position: absolute; left: 0; - top: var(--spacing-l); opacity: 0; transition: opacity 100ms ease-in; + color: var(--icon-base-color); +} + +.defaultTopPadding { + top: var(--spacing-m); +} +.extraTopPadding { + top: calc(14px + var(--spacing-m)); } diff --git a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue index 6b58fb4290..41ef0f7430 100644 --- a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue +++ b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue @@ -23,6 +23,7 @@ import Condition from './Condition.vue'; import CombinatorSelect from './CombinatorSelect.vue'; import { resolveParameter } from '@/composables/useWorkflowHelpers'; import { v4 as uuid } from 'uuid'; +import Draggable from 'vuedraggable'; interface Props { parameter: INodeProperties; @@ -161,30 +162,41 @@ function getIssues(index: number): string[] {
-
- + + +
.combinator { + display: none; +} diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index de7359dfdd..59eacfdb10 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -17,6 +17,7 @@ import { N8nButton, } from 'n8n-design-system'; import ParameterInputList from './ParameterInputList.vue'; +import Draggable from 'vuedraggable'; const locale = useI18n(); @@ -126,42 +127,6 @@ const getOptionProperties = (optionName: string) => { return undefined; }; -const moveOptionDown = (optionName: string, index: number) => { - if (Array.isArray(mutableValues.value[optionName])) { - mutableValues.value[optionName].splice( - index + 1, - 0, - mutableValues.value[optionName].splice(index, 1)[0], - ); - } - - const parameterData: ValueChangedEvent = { - name: getPropertyPath(optionName), - value: mutableValues.value[optionName], - type: 'optionsOrderChanged', - }; - - emit('valueChanged', parameterData); -}; - -const moveOptionUp = (optionName: string, index: number) => { - if (Array.isArray(mutableValues.value[optionName])) { - mutableValues.value?.[optionName].splice( - index - 1, - 0, - mutableValues.value[optionName].splice(index, 1)[0], - ); - } - - const parameterData: ValueChangedEvent = { - name: getPropertyPath(optionName), - value: mutableValues.value[optionName], - type: 'optionsOrderChanged', - }; - - emit('valueChanged', parameterData); -}; - const optionSelected = (optionName: string) => { const option = getOptionProperties(optionName); if (option === undefined) { @@ -219,6 +184,15 @@ const optionSelected = (optionName: string) => { const valueChanged = (parameterData: IUpdateInformation) => { emit('valueChanged', parameterData); }; +const onDragChange = (optionName: string) => { + const parameterData: ValueChangedEvent = { + name: getPropertyPath(optionName), + value: mutableValues.value[optionName], + type: 'optionsOrderChanged', + }; + + emit('valueChanged', parameterData); +}; @@ -114,6 +140,11 @@ const displayProjects = computed(() => width: 100%; overflow: hidden; align-items: start; + &:hover { + .plusBtn { + display: block; + } + } } .projectItems { @@ -132,12 +163,40 @@ const displayProjects = computed(() => } .projectsLabel { - margin: 0 var(--spacing-xs) var(--spacing-s); + display: flex; + justify-content: space-between; + margin: 0 0 var(--spacing-s) var(--spacing-xs); padding: 0 var(--spacing-s); text-overflow: ellipsis; overflow: hidden; box-sizing: border-box; color: var(--color-text-base); + + &.collapsed { + padding: 0; + margin-left: 0; + justify-content: center; + } +} + +.plusBtn { + margin: 0; + padding: 0; + color: var(--color-text-lighter); + display: none; +} + +.addFirstProjectBtn { + border: 1px solid var(--color-background-dark); + font-size: var(--font-size-xs); + padding: var(--spacing-3xs); + margin: 0 var(--spacing-m) var(--spacing-m); + + &.collapsed { + > span:last-child { + display: none; + } + } } diff --git a/packages/editor-ui/src/composables/useGlobalEntityCreation.ts b/packages/editor-ui/src/composables/useGlobalEntityCreation.ts index 6427bae1bc..bf1efb6691 100644 --- a/packages/editor-ui/src/composables/useGlobalEntityCreation.ts +++ b/packages/editor-ui/src/composables/useGlobalEntityCreation.ts @@ -1,4 +1,4 @@ -import { computed } from 'vue'; +import { computed, ref } from 'vue'; import { VIEWS } from '@/constants'; import { useRouter } from 'vue-router'; import { useI18n } from '@/composables/useI18n'; @@ -35,6 +35,9 @@ export const useGlobalEntityCreation = () => { const router = useRouter(); const i18n = useI18n(); const toast = useToast(); + + const isCreatingProject = ref(false); + const displayProjects = computed(() => sortByProperty( 'name', @@ -156,6 +159,8 @@ export const useGlobalEntityCreation = () => { }); const createProject = async () => { + isCreatingProject.value = true; + try { const newProject = await projectsStore.createProject({ name: i18n.baseText('projects.settings.newProjectName'), @@ -169,6 +174,8 @@ export const useGlobalEntityCreation = () => { }); } catch (error) { toast.showError(error, i18n.baseText('projects.error.title')); + } finally { + isCreatingProject.value = false; } }; @@ -226,5 +233,8 @@ export const useGlobalEntityCreation = () => { createProjectAppendSlotName, projectsLimitReachedMessage, upgradeLabel, + createProject, + isCreatingProject, + displayProjects, }; }; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index d7dd178d93..638000b219 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2540,7 +2540,7 @@ "projects.menu.overview": "Overview", "projects.menu.title": "Projects", "projects.menu.personal": "Personal", - "projects.menu.addProject": "Add project", + "projects.menu.addFirstProject": "Add first project", "projects.settings": "Project settings", "projects.settings.newProjectName": "My project", "projects.settings.name": "Project name", From e0dc385f8bc8ee13fbc5bbf35e07654e52b193e9 Mon Sep 17 00:00:00 2001 From: Marc Littlemore Date: Thu, 19 Dec 2024 08:13:17 +0000 Subject: [PATCH 07/25] feat(API): Exclude pinned data from workflows (#12261) --- .../src/databases/entities/workflow-entity.ts | 2 +- packages/cli/src/public-api/types.ts | 3 +- .../workflows/spec/paths/workflows.id.yml | 7 +++ .../workflows/spec/paths/workflows.yml | 7 +++ .../handlers/workflows/workflows.handler.ts | 21 ++++++- .../integration/public-api/workflows.test.ts | 61 +++++++++++++++++++ 6 files changed, 98 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index b03cf2c28d..67d0f0e345 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -80,7 +80,7 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl nullable: true, transformer: sqlite.jsonColumn, }) - pinData: ISimplifiedPinData; + pinData?: ISimplifiedPinData; @Column({ length: 36 }) versionId: string; diff --git a/packages/cli/src/public-api/types.ts b/packages/cli/src/public-api/types.ts index 327d363073..b10d2f81bd 100644 --- a/packages/cli/src/public-api/types.ts +++ b/packages/cli/src/public-api/types.ts @@ -74,11 +74,12 @@ export declare namespace WorkflowRequest { active: boolean; name?: string; projectId?: string; + excludePinnedData?: boolean; } >; type Create = AuthenticatedRequest<{}, {}, WorkflowEntity, {}>; - type Get = AuthenticatedRequest<{ id: string }, {}, {}, {}>; + type Get = AuthenticatedRequest<{ id: string }, {}, {}, { excludePinnedData?: boolean }>; type Delete = Get; type Update = AuthenticatedRequest<{ id: string }, {}, WorkflowEntity, {}>; type Activate = Get; diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml index 37cad74c86..c8b2bf51cd 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.id.yml @@ -6,6 +6,13 @@ get: summary: Retrieves a workflow description: Retrieves a workflow. parameters: + - name: excludePinnedData + in: query + required: false + description: Set this to avoid retrieving pinned data + schema: + type: boolean + example: true - $ref: '../schemas/parameters/workflowId.yml' responses: '200': diff --git a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml index 1024e36cb5..4b3bc5e069 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml +++ b/packages/cli/src/public-api/v1/handlers/workflows/spec/paths/workflows.yml @@ -60,6 +60,13 @@ get: schema: type: string example: VmwOO9HeTEj20kxM + - name: excludePinnedData + in: query + required: false + description: Set this to avoid retrieving pinned data + schema: + type: boolean + example: true - $ref: '../../../../shared/spec/parameters/limit.yml' - $ref: '../../../../shared/spec/parameters/cursor.yml' responses: diff --git a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts index b0956a15c1..7a9003dc28 100644 --- a/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts @@ -105,6 +105,7 @@ export = { projectScope('workflow:read', 'workflow'), async (req: WorkflowRequest.Get, res: express.Response): Promise => { const { id } = req.params; + const { excludePinnedData = false } = req.query; const workflow = await Container.get(SharedWorkflowRepository).findWorkflowForUser( id, @@ -120,6 +121,10 @@ export = { return res.status(404).json({ message: 'Not Found' }); } + if (excludePinnedData) { + delete workflow.pinData; + } + Container.get(EventService).emit('user-retrieved-workflow', { userId: req.user.id, publicApi: true, @@ -131,7 +136,15 @@ export = { getWorkflows: [ validCursor, async (req: WorkflowRequest.GetAll, res: express.Response): Promise => { - const { offset = 0, limit = 100, active, tags, name, projectId } = req.query; + const { + offset = 0, + limit = 100, + excludePinnedData = false, + active, + tags, + name, + projectId, + } = req.query; const where: FindOptionsWhere = { ...(active !== undefined && { active }), @@ -199,6 +212,12 @@ export = { ...(!config.getEnv('workflowTagsDisabled') && { relations: ['tags'] }), }); + if (excludePinnedData) { + workflows.forEach((workflow) => { + delete workflow.pinData; + }); + } + Container.get(EventService).emit('user-retrieved-all-workflows', { userId: req.user.id, publicApi: true, diff --git a/packages/cli/test/integration/public-api/workflows.test.ts b/packages/cli/test/integration/public-api/workflows.test.ts index 5425455aca..28f9d444da 100644 --- a/packages/cli/test/integration/public-api/workflows.test.ts +++ b/packages/cli/test/integration/public-api/workflows.test.ts @@ -378,6 +378,47 @@ describe('GET /workflows', () => { expect(updatedAt).toBeDefined(); } }); + + test('should return all owned workflows without pinned data', async () => { + await Promise.all([ + createWorkflow( + { + pinData: { + Webhook1: [{ json: { first: 'first' } }], + }, + }, + member, + ), + createWorkflow( + { + pinData: { + Webhook2: [{ json: { second: 'second' } }], + }, + }, + member, + ), + createWorkflow( + { + pinData: { + Webhook3: [{ json: { third: 'third' } }], + }, + }, + member, + ), + ]); + + const response = await authMemberAgent.get('/workflows?excludePinnedData=true'); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(3); + expect(response.body.nextCursor).toBeNull(); + + for (const workflow of response.body.data) { + const { pinData } = workflow; + + expect(pinData).not.toBeDefined(); + } + }); }); describe('GET /workflows/:id', () => { @@ -444,6 +485,26 @@ describe('GET /workflows/:id', () => { expect(createdAt).toEqual(workflow.createdAt.toISOString()); expect(updatedAt).toEqual(workflow.updatedAt.toISOString()); }); + + test('should retrieve workflow without pinned data', async () => { + // create and assign workflow to owner + const workflow = await createWorkflow( + { + pinData: { + Webhook1: [{ json: { first: 'first' } }], + }, + }, + member, + ); + + const response = await authMemberAgent.get(`/workflows/${workflow.id}?excludePinnedData=true`); + + expect(response.statusCode).toBe(200); + + const { pinData } = response.body; + + expect(pinData).not.toBeDefined(); + }); }); describe('DELETE /workflows/:id', () => { From a99d726f42d027b64f94eda0d385b597c5d5be2e Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:55:50 +0200 Subject: [PATCH 08/25] fix(core): Fix serialization of circular json with task runner (#12288) --- .../__tests__/task-runner-ws-server.test.ts | 24 +++++++++++++++++++ packages/cli/src/runners/runner-ws-server.ts | 4 ++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/runners/__tests__/task-runner-ws-server.test.ts b/packages/cli/src/runners/__tests__/task-runner-ws-server.test.ts index b092e08fed..24b12fa190 100644 --- a/packages/cli/src/runners/__tests__/task-runner-ws-server.test.ts +++ b/packages/cli/src/runners/__tests__/task-runner-ws-server.test.ts @@ -58,4 +58,28 @@ describe('TaskRunnerWsServer', () => { expect(clearIntervalSpy).toHaveBeenCalled(); }); }); + + describe('sendMessage', () => { + it('should work with a message containing circular references', () => { + const server = new TaskRunnerWsServer(mock(), mock(), mock(), mock(), mock()); + const ws = mock(); + server.runnerConnections.set('test-runner', ws); + + const messageData: Record = {}; + messageData.circular = messageData; + + expect(() => + server.sendMessage('test-runner', { + type: 'broker:taskdataresponse', + taskId: 'taskId', + requestId: 'requestId', + data: messageData, + }), + ).not.toThrow(); + + expect(ws.send).toHaveBeenCalledWith( + '{"type":"broker:taskdataresponse","taskId":"taskId","requestId":"requestId","data":{"circular":"[Circular Reference]"}}', + ); + }); + }); }); diff --git a/packages/cli/src/runners/runner-ws-server.ts b/packages/cli/src/runners/runner-ws-server.ts index 3a5fa53029..8ea3a7edbe 100644 --- a/packages/cli/src/runners/runner-ws-server.ts +++ b/packages/cli/src/runners/runner-ws-server.ts @@ -1,6 +1,6 @@ import { TaskRunnersConfig } from '@n8n/config'; import type { BrokerMessage, RunnerMessage } from '@n8n/task-runner'; -import { ApplicationError } from 'n8n-workflow'; +import { ApplicationError, jsonStringify } from 'n8n-workflow'; import { Service } from 'typedi'; import type WebSocket from 'ws'; @@ -83,7 +83,7 @@ export class TaskRunnerWsServer { } sendMessage(id: TaskRunner['id'], message: BrokerMessage.ToRunner.All) { - this.runnerConnections.get(id)?.send(JSON.stringify(message)); + this.runnerConnections.get(id)?.send(jsonStringify(message, { replaceCircularRefs: true })); } add(id: TaskRunner['id'], connection: WebSocket) { From 882484e8ee7d1841d5d600414ca48e9915abcfa8 Mon Sep 17 00:00:00 2001 From: Stanimira Rikova <104592468+Stamsy@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:06:46 +0200 Subject: [PATCH 09/25] feat: Add solarwinds ipam credentials (#12005) --- .../SolarWindsIpamApi.credentials.ts | 85 +++++++++++++++++++ .../credentials/icons/SolarWindsIpam.svg | 71 ++++++++++++++++ packages/nodes-base/package.json | 1 + 3 files changed, 157 insertions(+) create mode 100644 packages/nodes-base/credentials/SolarWindsIpamApi.credentials.ts create mode 100644 packages/nodes-base/credentials/icons/SolarWindsIpam.svg diff --git a/packages/nodes-base/credentials/SolarWindsIpamApi.credentials.ts b/packages/nodes-base/credentials/SolarWindsIpamApi.credentials.ts new file mode 100644 index 0000000000..2980978031 --- /dev/null +++ b/packages/nodes-base/credentials/SolarWindsIpamApi.credentials.ts @@ -0,0 +1,85 @@ +import type { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class SolarWindsIpamApi implements ICredentialType { + name = 'solarWindsIpamApi'; + + displayName = 'SolarWinds IPAM'; + + documentationUrl = 'solarwindsipam'; + + icon = { + light: 'file:icons/SolarWindsIpam.svg', + dark: 'file:icons/SolarWindsIpam.svg', + } as const; + + httpRequestNode = { + name: 'SolarWinds IPAM', + docsUrl: 'https://www.solarwinds.com/ip-address-manager', + apiBaseUrlPlaceholder: 'https://your-ipam-server', + }; + + properties: INodeProperties[] = [ + { + displayName: 'Base URL', + name: 'url', + required: true, + type: 'string', + default: '', + placeholder: 'https://your-ipam-server', + description: 'The base URL of your SolarWinds IPAM server.', + }, + { + displayName: 'Username', + name: 'username', + required: true, + type: 'string', + default: '', + description: 'The username for SolarWinds IPAM API.', + }, + { + displayName: 'Password', + name: 'password', + required: true, + type: 'string', + typeOptions: { password: true }, + default: '', + description: 'The password for SolarWinds IPAM API.', + }, + ]; + + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + auth: { + username: '={{$credentials.username}}', + password: '={{$credentials.password}}', + }, + }, + }; + + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.url}}'.replace(/\/$/, ''), + url: '/SolarWinds/InformationService/v3/Json/Query', + method: 'GET', + qs: { + query: 'SELECT TOP 1 AccountID FROM IPAM.AccountRoles', + }, + skipSslCertificateValidation: true, + }, + rules: [ + { + type: 'responseCode', + properties: { + value: 403, + message: 'Connection failed: Invalid credentials or unreachable server', + }, + }, + ], + }; +} diff --git a/packages/nodes-base/credentials/icons/SolarWindsIpam.svg b/packages/nodes-base/credentials/icons/SolarWindsIpam.svg new file mode 100644 index 0000000000..814afbdb7c --- /dev/null +++ b/packages/nodes-base/credentials/icons/SolarWindsIpam.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e1ceaffe62..05e10b2993 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -318,6 +318,7 @@ "dist/credentials/Sms77Api.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/Snowflake.credentials.js", + "dist/credentials/SolarWindsIpamApi.credentials.js", "dist/credentials/SolarWindsObservabilityApi.credentials.js", "dist/credentials/SplunkApi.credentials.js", "dist/credentials/SpontitApi.credentials.js", From bbc2fc98dd1df114693c407f481160ec66465048 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:28:40 +0200 Subject: [PATCH 10/25] build: Fix broken lock file (#12297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- pnpm-lock.yaml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07e4f3faa6..14b1b67a61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,7 +283,7 @@ importers: version: 4.0.7 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 dotenv: specifier: 8.6.0 version: 8.6.0 @@ -354,7 +354,7 @@ importers: dependencies: axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 packages/@n8n/codemirror-lang: dependencies: @@ -428,7 +428,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -455,7 +455,7 @@ importers: version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.15 - version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe) + version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm) '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -542,7 +542,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.6 - version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) lodash: specifier: 'catalog:' version: 4.17.21 @@ -801,7 +801,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bcryptjs: specifier: 2.4.3 version: 2.4.3 @@ -1135,7 +1135,7 @@ importers: version: 1.11.0 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 chardet: specifier: 2.0.0 version: 2.0.0 @@ -1431,7 +1431,7 @@ importers: version: 10.11.0(vue@3.5.13(typescript@5.7.2)) axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 bowser: specifier: 2.11.0 version: 2.11.0 @@ -1929,7 +1929,7 @@ importers: version: 0.15.2 axios: specifier: 'catalog:' - version: 1.7.4(debug@4.3.7) + version: 1.7.4 callsites: specifier: 3.1.0 version: 3.1.0 @@ -15663,7 +15663,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15672,7 +15672,7 @@ snapshots: zod: 3.23.8 optionalDependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) transitivePeerDependencies: - encoding @@ -16136,7 +16136,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)': + '@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)': dependencies: '@ibm-cloud/watsonx-ai': 1.1.2 '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -16146,7 +16146,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) uuid: 10.0.0 zod: 3.23.8 @@ -16159,7 +16159,7 @@ snapshots: '@aws-sdk/client-s3': 3.666.0 '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -17170,7 +17170,7 @@ snapshots: '@rudderstack/rudder-sdk-node@2.0.9(tslib@2.6.2)': dependencies: - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 axios-retry: 3.7.0 component-type: 1.2.1 join-component: 1.1.0 @@ -19467,7 +19467,7 @@ snapshots: '@babel/runtime': 7.24.7 is-retry-allowed: 2.2.0 - axios@1.7.4(debug@4.3.7): + axios@1.7.4: dependencies: follow-redirects: 1.15.6(debug@4.3.6) form-data: 4.0.0 @@ -19475,9 +19475,9 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.7: + axios@1.7.4(debug@4.3.7): dependencies: - follow-redirects: 1.15.6(debug@4.3.6) + follow-redirects: 1.15.6(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -23343,7 +23343,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i): + langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu): dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) @@ -23367,7 +23367,7 @@ snapshots: '@langchain/groq': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/mistralai': 0.2.0(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) '@langchain/ollama': 0.1.2(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8))) - axios: 1.7.4(debug@4.3.7) + axios: 1.7.4 cheerio: 1.0.0 handlebars: 4.7.8 transitivePeerDependencies: From 38c5ed29325470b8f2e81f8838543ed2eb7b6ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 19 Dec 2024 10:51:32 +0100 Subject: [PATCH 11/25] chore(core): Upgrade launcher to 1.1.0 (#12283) --- docker/images/n8n-custom/Dockerfile | 2 +- docker/images/n8n/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/images/n8n-custom/Dockerfile b/docker/images/n8n-custom/Dockerfile index 2b72365eb8..13592140a4 100644 --- a/docker/images/n8n-custom/Dockerfile +++ b/docker/images/n8n-custom/Dockerfile @@ -33,7 +33,7 @@ COPY docker/images/n8n/docker-entrypoint.sh / # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=1.0.0 +ARG LAUNCHER_VERSION=1.1.0 COPY docker/images/n8n/n8n-task-runners.json /etc/n8n-task-runners.json # Download, verify, then extract the launcher binary RUN \ diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index 7407736185..10720c63f2 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -24,7 +24,7 @@ RUN set -eux; \ # Setup the Task Runner Launcher ARG TARGETPLATFORM -ARG LAUNCHER_VERSION=1.0.0 +ARG LAUNCHER_VERSION=1.1.0 COPY n8n-task-runners.json /etc/n8n-task-runners.json # Download, verify, then extract the launcher binary RUN \ From 64c0414ef28acf0f7ec42b4b0bb21cbf2921ebe7 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, 19 Dec 2024 10:58:26 +0100 Subject: [PATCH 12/25] fix(Redis Node): Add support for username auth (#12274) --- .../credentials/Redis.credentials.ts | 7 ++ packages/nodes-base/nodes/Redis/Redis.node.ts | 3 +- .../nodes/Redis/RedisTrigger.node.ts | 3 +- .../{test => __tests__}/Redis.node.test.ts | 85 +++++++++++++++++-- .../RedisTrigger.node.test.ts | 5 +- packages/nodes-base/nodes/Redis/types.ts | 12 +++ packages/nodes-base/nodes/Redis/utils.ts | 28 +++--- 7 files changed, 118 insertions(+), 25 deletions(-) rename packages/nodes-base/nodes/Redis/{test => __tests__}/Redis.node.test.ts (83%) rename packages/nodes-base/nodes/Redis/{test => __tests__}/RedisTrigger.node.test.ts (97%) create mode 100644 packages/nodes-base/nodes/Redis/types.ts diff --git a/packages/nodes-base/credentials/Redis.credentials.ts b/packages/nodes-base/credentials/Redis.credentials.ts index 194c2f59e3..9d9bbab394 100644 --- a/packages/nodes-base/credentials/Redis.credentials.ts +++ b/packages/nodes-base/credentials/Redis.credentials.ts @@ -17,6 +17,13 @@ export class Redis implements ICredentialType { }, default: '', }, + { + displayName: 'User', + name: 'user', + type: 'string', + default: '', + hint: 'Leave blank for password-only auth', + }, { displayName: 'Host', name: 'host', diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index ccd826eb89..014c28d3d6 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -15,6 +15,7 @@ import { getValue, setValue, } from './utils'; +import type { RedisCredential } from './types'; export class Redis implements INodeType { description: INodeTypeDescription = { @@ -512,7 +513,7 @@ export class Redis implements INodeType { // have a parameter field for a path. Because it is not possible to set // array, object via parameter directly (should maybe be possible?!?!) // Should maybe have a parameter which is JSON. - const credentials = await this.getCredentials('redis'); + const credentials = await this.getCredentials('redis'); const client = setupRedisClient(credentials); await client.connect(); diff --git a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts index 34b4bb8d62..680d19b026 100644 --- a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts +++ b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts @@ -7,6 +7,7 @@ import type { import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { redisConnectionTest, setupRedisClient } from './utils'; +import type { RedisCredential } from './types'; interface Options { jsonParseBody: boolean; @@ -74,7 +75,7 @@ export class RedisTrigger implements INodeType { }; async trigger(this: ITriggerFunctions): Promise { - const credentials = await this.getCredentials('redis'); + const credentials = await this.getCredentials('redis'); const channels = (this.getNodeParameter('channels') as string).split(','); const options = this.getNodeParameter('options') as Options; diff --git a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts b/packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts similarity index 83% rename from packages/nodes-base/nodes/Redis/test/Redis.node.test.ts rename to packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts index 6339459eb1..789aef5242 100644 --- a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts +++ b/packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts @@ -1,18 +1,24 @@ -import type { RedisClientType } from '@redis/client'; import { mock } from 'jest-mock-extended'; -import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow'; +import type { + ICredentialsDecrypted, + ICredentialTestFunctions, + IExecuteFunctions, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; -const mockClient = mock(); +const mockClient = mock(); const createClient = jest.fn().mockReturnValue(mockClient); jest.mock('redis', () => ({ createClient })); import { Redis } from '../Redis.node'; -import { setupRedisClient } from '../utils'; +import { redisConnectionTest, setupRedisClient } from '../utils'; +import type { RedisClient } from '../types'; describe('Redis Node', () => { const node = new Redis(); beforeEach(() => { + jest.clearAllMocks(); createClient.mockReturnValue(mockClient); }); @@ -27,7 +33,6 @@ describe('Redis Node', () => { }); expect(createClient).toHaveBeenCalledWith({ database: 0, - password: undefined, socket: { host: 'redis.domain', port: 1234, @@ -45,7 +50,6 @@ describe('Redis Node', () => { }); expect(createClient).toHaveBeenCalledWith({ database: 0, - password: undefined, socket: { host: 'redis.domain', port: 1234, @@ -53,6 +57,75 @@ describe('Redis Node', () => { }, }); }); + + it('should set user on auth', () => { + setupRedisClient({ + host: 'redis.domain', + port: 1234, + database: 0, + user: 'test_user', + password: 'test_password', + }); + expect(createClient).toHaveBeenCalledWith({ + database: 0, + username: 'test_user', + password: 'test_password', + socket: { + host: 'redis.domain', + port: 1234, + tls: false, + }, + }); + }); + }); + + describe('redisConnectionTest', () => { + const thisArg = mock({}); + const credentials = mock({ + data: { + host: 'localhost', + port: 6379, + user: 'username', + password: 'password', + database: 0, + }, + }); + const redisOptions = { + socket: { + host: 'localhost', + port: 6379, + tls: false, + }, + database: 0, + username: 'username', + password: 'password', + }; + + it('should return success when connection is established', async () => { + const result = await redisConnectionTest.call(thisArg, credentials); + + expect(result).toEqual({ + status: 'OK', + message: 'Connection successful!', + }); + expect(createClient).toHaveBeenCalledWith(redisOptions); + expect(mockClient.connect).toHaveBeenCalled(); + expect(mockClient.ping).toHaveBeenCalled(); + }); + + it('should return error when connection fails', async () => { + mockClient.connect.mockRejectedValue(new Error('Connection failed')); + + const result = await redisConnectionTest.call(thisArg, credentials); + + expect(result).toEqual({ + status: 'Error', + message: 'Connection failed', + }); + expect(createClient).toHaveBeenCalledWith(redisOptions); + expect(mockClient.connect).toHaveBeenCalled(); + expect(mockClient.ping).not.toHaveBeenCalled(); + }); }); describe('operations', () => { diff --git a/packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts b/packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts similarity index 97% rename from packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts rename to packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts index eadad5fe90..97227fbe04 100644 --- a/packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts +++ b/packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts @@ -3,10 +3,11 @@ import { captor, mock } from 'jest-mock-extended'; import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow'; import { RedisTrigger } from '../RedisTrigger.node'; -import { type RedisClientType, setupRedisClient } from '../utils'; +import { setupRedisClient } from '../utils'; +import type { RedisClient } from '../types'; jest.mock('../utils', () => { - const mockRedisClient = mock(); + const mockRedisClient = mock(); return { setupRedisClient: jest.fn().mockReturnValue(mockRedisClient), }; diff --git a/packages/nodes-base/nodes/Redis/types.ts b/packages/nodes-base/nodes/Redis/types.ts new file mode 100644 index 0000000000..63e873acce --- /dev/null +++ b/packages/nodes-base/nodes/Redis/types.ts @@ -0,0 +1,12 @@ +import type { createClient } from 'redis'; + +export type RedisClient = ReturnType; + +export type RedisCredential = { + host: string; + port: number; + ssl?: boolean; + database: number; + user?: string; + password?: string; +}; diff --git a/packages/nodes-base/nodes/Redis/utils.ts b/packages/nodes-base/nodes/Redis/utils.ts index effdd99778..4364bab892 100644 --- a/packages/nodes-base/nodes/Redis/utils.ts +++ b/packages/nodes-base/nodes/Redis/utils.ts @@ -1,5 +1,4 @@ import type { - ICredentialDataDecryptedObject, ICredentialTestFunctions, ICredentialsDecrypted, IDataObject, @@ -7,29 +6,28 @@ import type { INodeCredentialTestResult, } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; +import { createClient } from 'redis'; -import { type RedisClientOptions, createClient } from 'redis'; -export type RedisClientType = ReturnType; +import type { RedisCredential, RedisClient } from './types'; -export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType { - const redisOptions: RedisClientOptions = { +export function setupRedisClient(credentials: RedisCredential): RedisClient { + return createClient({ socket: { - host: credentials.host as string, - port: credentials.port as number, + host: credentials.host, + port: credentials.port, tls: credentials.ssl === true, }, - database: credentials.database as number, - password: (credentials.password as string) || undefined, - }; - - return createClient(redisOptions); + database: credentials.database, + username: credentials.user || undefined, + password: credentials.password || undefined, + }); } export async function redisConnectionTest( this: ICredentialTestFunctions, credential: ICredentialsDecrypted, ): Promise { - const credentials = credential.data as ICredentialDataDecryptedObject; + const credentials = credential.data as RedisCredential; try { const client = setupRedisClient(credentials); @@ -88,7 +86,7 @@ export function convertInfoToObject(stringData: string): IDataObject { return returnData; } -export async function getValue(client: RedisClientType, keyName: string, type?: string) { +export async function getValue(client: RedisClient, keyName: string, type?: string) { if (type === undefined || type === 'automatic') { // Request the type first type = await client.type(keyName); @@ -107,7 +105,7 @@ export async function getValue(client: RedisClientType, keyName: string, type?: export async function setValue( this: IExecuteFunctions, - client: RedisClientType, + client: RedisClient, keyName: string, value: string | number | object | string[] | number[], expire: boolean, From 441d71e35d0d66cd06035dad1d44e66aee8f11a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:19:39 +0100 Subject: [PATCH 13/25] :rocket: Release 1.73.0 (#12302) Co-authored-by: r00gm <22072110+r00gm@users.noreply.github.com> --- CHANGELOG.md | 53 ++++++++++++++++++++++ package.json | 2 +- packages/@n8n/api-types/package.json | 2 +- packages/@n8n/config/package.json | 2 +- packages/@n8n/nodes-langchain/package.json | 2 +- packages/@n8n/task-runner/package.json | 2 +- packages/cli/package.json | 2 +- packages/core/package.json | 2 +- packages/design-system/package.json | 2 +- packages/editor-ui/package.json | 2 +- packages/node-dev/package.json | 2 +- packages/nodes-base/package.json | 2 +- packages/workflow/package.json | 2 +- 13 files changed, 65 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c49ea673f..9e5ffd4e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +# [1.73.0](https://github.com/n8n-io/n8n/compare/n8n@1.72.0...n8n@1.73.0) (2024-12-19) + + +### Bug Fixes + +* **core:** Ensure runners do not throw on unsupported console methods ([#12167](https://github.com/n8n-io/n8n/issues/12167)) ([57c6a61](https://github.com/n8n-io/n8n/commit/57c6a6167dd2b30f0082a416daefce994ecad33a)) +* **core:** Fix `$getWorkflowStaticData` on task runners ([#12153](https://github.com/n8n-io/n8n/issues/12153)) ([b479f14](https://github.com/n8n-io/n8n/commit/b479f14ef5012551b823bea5d2ffbddedfd50a77)) +* **core:** Fix binary data helpers (like `prepareBinaryData`) with task runner ([#12259](https://github.com/n8n-io/n8n/issues/12259)) ([0f1461f](https://github.com/n8n-io/n8n/commit/0f1461f2d5d7ec34236ed7fcec3e2f9ee7eb73c4)) +* **core:** Fix race condition in AI tool invocation with multiple items from the parent ([#12169](https://github.com/n8n-io/n8n/issues/12169)) ([dce0c58](https://github.com/n8n-io/n8n/commit/dce0c58f8605c33fc50ec8aa422f0fb5eee07637)) +* **core:** Fix serialization of circular json with task runner ([#12288](https://github.com/n8n-io/n8n/issues/12288)) ([a99d726](https://github.com/n8n-io/n8n/commit/a99d726f42d027b64f94eda0d385b597c5d5be2e)) +* **core:** Upgrade nanoid to address CVE-2024-55565 ([#12171](https://github.com/n8n-io/n8n/issues/12171)) ([8c0bd02](https://github.com/n8n-io/n8n/commit/8c0bd0200c386b122f495c453ccc97a001e4729c)) +* **editor:** Add new create first project CTA ([#12189](https://github.com/n8n-io/n8n/issues/12189)) ([878b419](https://github.com/n8n-io/n8n/commit/878b41904d76eda3ee230f850127b4d56993de24)) +* **editor:** Fix canvas ready opacity transition on new canvas ([#12264](https://github.com/n8n-io/n8n/issues/12264)) ([5d33a6b](https://github.com/n8n-io/n8n/commit/5d33a6ba8a2bccea097402fd04c0e2b00e423e76)) +* **editor:** Fix rendering of code-blocks in sticky notes ([#12227](https://github.com/n8n-io/n8n/issues/12227)) ([9b59035](https://github.com/n8n-io/n8n/commit/9b5903524b95bd21d5915908780942790cf88d27)) +* **editor:** Fix sticky color picker getting covered by nodes on new canvas ([#12263](https://github.com/n8n-io/n8n/issues/12263)) ([27bd3c8](https://github.com/n8n-io/n8n/commit/27bd3c85b3a4ddcf763a543b232069bb108130cf)) +* **editor:** Improve commit modal user facing messaging ([#12161](https://github.com/n8n-io/n8n/issues/12161)) ([ad39243](https://github.com/n8n-io/n8n/commit/ad392439826b17bd0b84f981e0958d88f09e7fe9)) +* **editor:** Prevent connection line from showing when clicking the plus button of a node ([#12265](https://github.com/n8n-io/n8n/issues/12265)) ([9180b46](https://github.com/n8n-io/n8n/commit/9180b46b52302b203eecf3bb81c3f2132527a1e6)) +* **editor:** Prevent stickies from being edited in preview mode in the new canvas ([#12222](https://github.com/n8n-io/n8n/issues/12222)) ([6706dcd](https://github.com/n8n-io/n8n/commit/6706dcdf72d54f33c1cf4956602c3a64a1578826)) +* **editor:** Reduce cases for Auto-Add of ChatTrigger for AI Agents ([#12154](https://github.com/n8n-io/n8n/issues/12154)) ([365e82d](https://github.com/n8n-io/n8n/commit/365e82d2008dff2f9c91664ee04d7a78363a8b30)) +* **editor:** Remove invalid connections after node handles change ([#12247](https://github.com/n8n-io/n8n/issues/12247)) ([6330bec](https://github.com/n8n-io/n8n/commit/6330bec4db0175b558f2747837323fdbb25b634a)) +* **editor:** Set dangerouslyUseHTMLString in composable ([#12280](https://github.com/n8n-io/n8n/issues/12280)) ([6ba91b5](https://github.com/n8n-io/n8n/commit/6ba91b5e1ed197c67146347a6f6e663ecdf3de48)) +* **editor:** Set RunData outputIndex based on incoming data ([#12182](https://github.com/n8n-io/n8n/issues/12182)) ([dc4261a](https://github.com/n8n-io/n8n/commit/dc4261ae7eca6cf277404cd514c90fad42f14ae0)) +* **editor:** Update the universal create button interaction ([#12105](https://github.com/n8n-io/n8n/issues/12105)) ([5300e0a](https://github.com/n8n-io/n8n/commit/5300e0ac45bf832b3d2957198a49a1c687f3fe1f)) +* **Elasticsearch Node:** Fix issue stopping search queries being sent ([#11464](https://github.com/n8n-io/n8n/issues/11464)) ([388a83d](https://github.com/n8n-io/n8n/commit/388a83dfbdc6ac301e4df704666df9f09fb7d0b3)) +* **Extract from File Node:** Detect file encoding ([#12081](https://github.com/n8n-io/n8n/issues/12081)) ([92af245](https://github.com/n8n-io/n8n/commit/92af245d1aab5bfad8618fda69b2405f5206875d)) +* **Github Node:** Fix fetch of file names with ? character ([#12206](https://github.com/n8n-io/n8n/issues/12206)) ([39462ab](https://github.com/n8n-io/n8n/commit/39462abe1fde7e82b5e5b8f3ceebfcadbfd7c925)) +* **Invoice Ninja Node:** Fix actions for bank transactions ([#11511](https://github.com/n8n-io/n8n/issues/11511)) ([80eea49](https://github.com/n8n-io/n8n/commit/80eea49cf0bf9db438eb85af7cd22aeb11fbfed2)) +* **Linear Node:** Fix issue with error handling ([#12191](https://github.com/n8n-io/n8n/issues/12191)) ([b8eae5f](https://github.com/n8n-io/n8n/commit/b8eae5f28a7d523195f4715cd8da77b3a884ae4c)) +* **MongoDB Node:** Fix checks on projection feature call ([#10563](https://github.com/n8n-io/n8n/issues/10563)) ([58bab46](https://github.com/n8n-io/n8n/commit/58bab461c4c5026b2ca5ea143cbcf98bf3a4ced8)) +* **Postgres Node:** Allow users to wrap strings with $$ ([#12034](https://github.com/n8n-io/n8n/issues/12034)) ([0c15e30](https://github.com/n8n-io/n8n/commit/0c15e30778cc5cb10ed368df144d6fbb2504ec70)) +* **Redis Node:** Add support for username auth ([#12274](https://github.com/n8n-io/n8n/issues/12274)) ([64c0414](https://github.com/n8n-io/n8n/commit/64c0414ef28acf0f7ec42b4b0bb21cbf2921ebe7)) + + +### Features + +* Add solarwinds ipam credentials ([#12005](https://github.com/n8n-io/n8n/issues/12005)) ([882484e](https://github.com/n8n-io/n8n/commit/882484e8ee7d1841d5d600414ca48e9915abcfa8)) +* Add SolarWinds Observability node credentials ([#11805](https://github.com/n8n-io/n8n/issues/11805)) ([e8a5db5](https://github.com/n8n-io/n8n/commit/e8a5db5beb572edbb61dd9100b70827ccc4cca58)) +* **AI Agent Node:** Update descriptions and titles for Chat Trigger options in AI Agents and Memory ([#12155](https://github.com/n8n-io/n8n/issues/12155)) ([07a6ae1](https://github.com/n8n-io/n8n/commit/07a6ae11b3291c1805553d55ba089fe8dd919fd8)) +* **API:** Exclude pinned data from workflows ([#12261](https://github.com/n8n-io/n8n/issues/12261)) ([e0dc385](https://github.com/n8n-io/n8n/commit/e0dc385f8bc8ee13fbc5bbf35e07654e52b193e9)) +* **editor:** Params pane collection improvements ([#11607](https://github.com/n8n-io/n8n/issues/11607)) ([6e44c71](https://github.com/n8n-io/n8n/commit/6e44c71c9ca82cce20eb55bb9003930bbf66a16c)) +* **editor:** Support adding nodes via drag and drop from node creator on new canvas ([#12197](https://github.com/n8n-io/n8n/issues/12197)) ([1bfd9c0](https://github.com/n8n-io/n8n/commit/1bfd9c0e913f3eefc4593f6c344db1ae1f6e4df4)) +* **Facebook Graph API Node:** Update node to support API v21.0 ([#12116](https://github.com/n8n-io/n8n/issues/12116)) ([14c33f6](https://github.com/n8n-io/n8n/commit/14c33f666fe92f7173e4f471fb478e629e775c62)) +* **Linear Trigger Node:** Add support for admin scope ([#12211](https://github.com/n8n-io/n8n/issues/12211)) ([410ea9a](https://github.com/n8n-io/n8n/commit/410ea9a2ef2e14b5e8e4493e5db66cfc2290d8f6)) +* **MailerLite Node:** Update node to support new api ([#11933](https://github.com/n8n-io/n8n/issues/11933)) ([d6b8e65](https://github.com/n8n-io/n8n/commit/d6b8e65abeb411f86538c1630dcce832ee0846a9)) +* Send and wait operation - freeText and customForm response types ([#12106](https://github.com/n8n-io/n8n/issues/12106)) ([e98c7f1](https://github.com/n8n-io/n8n/commit/e98c7f160b018243dc88490d46fb1047a4d7fcdc)) + + +### Performance Improvements + +* **editor:** SchemaView performance improvement by ≈90% 🚀 ([#12180](https://github.com/n8n-io/n8n/issues/12180)) ([6a58309](https://github.com/n8n-io/n8n/commit/6a5830959f5fb493a4119869b8298d8ed702c84a)) + + + # [1.72.0](https://github.com/n8n-io/n8n/compare/n8n@1.71.0...n8n@1.72.0) (2024-12-11) diff --git a/package.json b/package.json index 29be8d868a..a42a17ee44 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.72.0", + "version": "1.73.0", "private": true, "engines": { "node": ">=20.15", diff --git a/packages/@n8n/api-types/package.json b/packages/@n8n/api-types/package.json index fac0011437..c14e189922 100644 --- a/packages/@n8n/api-types/package.json +++ b/packages/@n8n/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/api-types", - "version": "0.10.0", + "version": "0.11.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index 961079acd6..c4368a75c5 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/config", - "version": "1.22.0", + "version": "1.23.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 05715fcf7c..f19b8c0058 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.72.0", + "version": "1.73.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index f82692db77..212909990e 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/task-runner", - "version": "1.10.0", + "version": "1.11.0", "scripts": { "clean": "rimraf dist .turbo", "start": "node dist/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 317aeb0d9c..8e9ff0f7ca 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.72.0", + "version": "1.73.0", "description": "n8n Workflow Automation Tool", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/core/package.json b/packages/core/package.json index 84cd8f7c8a..26b41800d9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.72.0", + "version": "1.73.0", "description": "Core functionality of n8n", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 47e5a2d13e..d4a7e1dcec 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.62.0", + "version": "1.63.0", "main": "src/main.ts", "import": "src/main.ts", "scripts": { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 68b13e13f4..3293abfe3b 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.72.0", + "version": "1.73.0", "description": "Workflow Editor UI for n8n", "main": "index.js", "scripts": { diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index 6a08e4b96c..96e133aa17 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -1,6 +1,6 @@ { "name": "n8n-node-dev", - "version": "1.72.0", + "version": "1.73.0", "description": "CLI to simplify n8n credentials/node development", "main": "dist/src/index", "types": "dist/src/index.d.ts", diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 05e10b2993..db7ef72af3 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "1.72.0", + "version": "1.73.0", "description": "Base nodes of n8n", "main": "index.js", "scripts": { diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 9254422b90..cabbeaaaaa 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "1.71.0", + "version": "1.72.0", "description": "Workflow base code of n8n", "main": "dist/index.js", "module": "src/index.ts", From 01b781a10828ca2c4cf32762373ad40904c02d2c Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 19 Dec 2024 13:05:12 +0100 Subject: [PATCH 14/25] fix(editor): Nodes' icon color in dark mode (#12279) --- .../nodes/chains/ChainLLM/ChainLlm.node.ts | 1 + .../ChainRetrievalQA/ChainRetrievalQa.node.ts | 1 + .../ChainSummarization.node.ts | 1 + .../MemoryBufferWindow.node.ts | 1 + .../MemoryChatRetriever.node.ts | 1 + .../MemoryMotorhead/MemoryMotorhead.node.ts | 1 + .../OutputParserAutofixing.node.ts | 1 + .../OutputParserItemList.node.ts | 1 + .../OutputParserStructured.node.ts | 1 + .../RetrieverContextualCompression.node.ts | 1 + .../RetrieverMultiQuery.node.ts | 1 + .../RetrieverVectorStore.node.ts | 1 + .../RetrieverWorkflow.node.ts | 1 + .../TextSplitterCharacterTextSplitter.node.ts | 1 + ...tterRecursiveCharacterTextSplitter.node.ts | 1 + .../TextSplitterTokenSplitter.node.ts | 1 + .../ToolCalculator/ToolCalculator.node.ts | 1 + .../nodes/tools/ToolCode/ToolCode.node.ts | 1 + .../ToolVectorStore/ToolVectorStore.node.ts | 1 + .../tools/ToolWorkflow/ToolWorkflow.node.ts | 1 + .../VectorStoreInMemory.node.ts | 1 + .../VectorStorePinecone.node.ts | 2 +- .../VectorStorePinecone/pinecone.dark.svg | 21 ++++++++++++++++++ .../VectorStorePinecone/pinecone.svg | 22 ++++++++++++++++++- .../shared/createVectorStoreNode.ts | 3 +++ 25 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.dark.svg diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts index d4e205ec88..4b2ddf5db9 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts @@ -254,6 +254,7 @@ export class ChainLlm implements INodeType { displayName: 'Basic LLM Chain', name: 'chainLlm', icon: 'fa:link', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3, 1.4, 1.5], description: 'A simple chain to prompt a large language model', diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts index 9c7c739701..7829bc7813 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts @@ -31,6 +31,7 @@ export class ChainRetrievalQa implements INodeType { displayName: 'Question and Answer Chain', name: 'chainRetrievalQa', icon: 'fa:link', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3, 1.4], description: 'Answer questions about retrieved documents', diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts index cd47eb6a15..9c97190952 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts @@ -10,6 +10,7 @@ export class ChainSummarization extends VersionedNodeType { displayName: 'Summarization Chain', name: 'chainSummarization', icon: 'fa:link', + iconColor: 'black', group: ['transform'], description: 'Transforms text into a concise summary', codex: { diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts index 480bed68f9..3bdcd0d1fd 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts @@ -78,6 +78,7 @@ export class MemoryBufferWindow implements INodeType { displayName: 'Window Buffer Memory (easiest)', name: 'memoryBufferWindow', icon: 'fa:database', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3], description: 'Stores in n8n memory, so no credentials required', diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryChatRetriever/MemoryChatRetriever.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryChatRetriever/MemoryChatRetriever.node.ts index fa54f25a16..82fcba22a6 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryChatRetriever/MemoryChatRetriever.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryChatRetriever/MemoryChatRetriever.node.ts @@ -38,6 +38,7 @@ export class MemoryChatRetriever implements INodeType { displayName: 'Chat Messages Retriever', name: 'memoryChatRetriever', icon: 'fa:database', + iconColor: 'black', group: ['transform'], hidden: true, version: 1, diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.ts index f5184d7e93..06fa387ee6 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.ts @@ -19,6 +19,7 @@ export class MemoryMotorhead implements INodeType { displayName: 'Motorhead', name: 'memoryMotorhead', icon: 'fa:file-export', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3], description: 'Use Motorhead Memory', diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/OutputParserAutofixing.node.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/OutputParserAutofixing.node.ts index 0ccf4c27c0..f9e6cd2968 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/OutputParserAutofixing.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserAutofixing/OutputParserAutofixing.node.ts @@ -21,6 +21,7 @@ export class OutputParserAutofixing implements INodeType { displayName: 'Auto-fixing Output Parser', name: 'outputParserAutofixing', icon: 'fa:tools', + iconColor: 'black', group: ['transform'], version: 1, description: 'Automatically fix the output if it is not in the correct format', diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/OutputParserItemList.node.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/OutputParserItemList.node.ts index 696a6be79c..b94b82fada 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/OutputParserItemList.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserItemList/OutputParserItemList.node.ts @@ -15,6 +15,7 @@ export class OutputParserItemList implements INodeType { displayName: 'Item List Output Parser', name: 'outputParserItemList', icon: 'fa:bars', + iconColor: 'black', group: ['transform'], version: 1, description: 'Return the results as separate items', diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts index 8da4cb05d8..0869020997 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts @@ -20,6 +20,7 @@ export class OutputParserStructured implements INodeType { displayName: 'Structured Output Parser', name: 'outputParserStructured', icon: 'fa:code', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2], defaultVersion: 1.2, diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverContextualCompression/RetrieverContextualCompression.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverContextualCompression/RetrieverContextualCompression.node.ts index 74db608551..feb70ecb43 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverContextualCompression/RetrieverContextualCompression.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverContextualCompression/RetrieverContextualCompression.node.ts @@ -19,6 +19,7 @@ export class RetrieverContextualCompression implements INodeType { displayName: 'Contextual Compression Retriever', name: 'retrieverContextualCompression', icon: 'fa:box-open', + iconColor: 'black', group: ['transform'], version: 1, description: 'Enhances document similarity search by contextual compression.', diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverMultiQuery/RetrieverMultiQuery.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverMultiQuery/RetrieverMultiQuery.node.ts index 3805eb5374..4bbc45f6d1 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverMultiQuery/RetrieverMultiQuery.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverMultiQuery/RetrieverMultiQuery.node.ts @@ -18,6 +18,7 @@ export class RetrieverMultiQuery implements INodeType { displayName: 'MultiQuery Retriever', name: 'retrieverMultiQuery', icon: 'fa:box-open', + iconColor: 'black', group: ['transform'], version: 1, description: diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverVectorStore/RetrieverVectorStore.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverVectorStore/RetrieverVectorStore.node.ts index 74f88e5561..915d9766dc 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverVectorStore/RetrieverVectorStore.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverVectorStore/RetrieverVectorStore.node.ts @@ -15,6 +15,7 @@ export class RetrieverVectorStore implements INodeType { displayName: 'Vector Store Retriever', name: 'retrieverVectorStore', icon: 'fa:box-open', + iconColor: 'black', group: ['transform'], version: 1, description: 'Use a Vector Store as Retriever', diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts index 5e9fecd47a..1291b92252 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts @@ -41,6 +41,7 @@ export class RetrieverWorkflow implements INodeType { displayName: 'Workflow Retriever', name: 'retrieverWorkflow', icon: 'fa:box-open', + iconColor: 'black', group: ['transform'], version: [1, 1.1], description: 'Use an n8n Workflow as Retriever', diff --git a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterCharacterTextSplitter/TextSplitterCharacterTextSplitter.node.ts b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterCharacterTextSplitter/TextSplitterCharacterTextSplitter.node.ts index c78bd39a6c..962af5bde2 100644 --- a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterCharacterTextSplitter/TextSplitterCharacterTextSplitter.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterCharacterTextSplitter/TextSplitterCharacterTextSplitter.node.ts @@ -17,6 +17,7 @@ export class TextSplitterCharacterTextSplitter implements INodeType { displayName: 'Character Text Splitter', name: 'textSplitterCharacterTextSplitter', icon: 'fa:grip-lines-vertical', + iconColor: 'black', group: ['transform'], version: 1, description: 'Split text into chunks by characters', diff --git a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterRecursiveCharacterTextSplitter/TextSplitterRecursiveCharacterTextSplitter.node.ts b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterRecursiveCharacterTextSplitter/TextSplitterRecursiveCharacterTextSplitter.node.ts index cfe8a32757..4e376c39a3 100644 --- a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterRecursiveCharacterTextSplitter/TextSplitterRecursiveCharacterTextSplitter.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterRecursiveCharacterTextSplitter/TextSplitterRecursiveCharacterTextSplitter.node.ts @@ -37,6 +37,7 @@ export class TextSplitterRecursiveCharacterTextSplitter implements INodeType { displayName: 'Recursive Character Text Splitter', name: 'textSplitterRecursiveCharacterTextSplitter', icon: 'fa:grip-lines-vertical', + iconColor: 'black', group: ['transform'], version: 1, description: 'Split text into chunks by characters recursively, recommended for most use cases', diff --git a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterTokenSplitter/TextSplitterTokenSplitter.node.ts b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterTokenSplitter/TextSplitterTokenSplitter.node.ts index cd881916d6..b5dade396d 100644 --- a/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterTokenSplitter/TextSplitterTokenSplitter.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/text_splitters/TextSplitterTokenSplitter/TextSplitterTokenSplitter.node.ts @@ -16,6 +16,7 @@ export class TextSplitterTokenSplitter implements INodeType { displayName: 'Token Splitter', name: 'textSplitterTokenSplitter', icon: 'fa:grip-lines-vertical', + iconColor: 'black', group: ['transform'], version: 1, description: 'Split text into chunks by tokens', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolCalculator/ToolCalculator.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolCalculator/ToolCalculator.node.ts index b3ed23c576..6d67a04555 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolCalculator/ToolCalculator.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolCalculator/ToolCalculator.node.ts @@ -16,6 +16,7 @@ export class ToolCalculator implements INodeType { displayName: 'Calculator', name: 'toolCalculator', icon: 'fa:calculator', + iconColor: 'black', group: ['transform'], version: 1, description: 'Make it easier for AI agents to perform arithmetic', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts index 214d4ed82a..029bce48f6 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolCode/ToolCode.node.ts @@ -26,6 +26,7 @@ export class ToolCode implements INodeType { displayName: 'Code Tool', name: 'toolCode', icon: 'fa:code', + iconColor: 'black', group: ['transform'], version: [1, 1.1], description: 'Write a tool in JS or Python', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts index 4b539e7e85..aaa2ca37d9 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolVectorStore/ToolVectorStore.node.ts @@ -18,6 +18,7 @@ export class ToolVectorStore implements INodeType { displayName: 'Vector Store Tool', name: 'toolVectorStore', icon: 'fa:database', + iconColor: 'black', group: ['transform'], version: [1], description: 'Retrieve context from vector store', diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 6b09cbfc88..227481b65c 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -32,6 +32,7 @@ export class ToolWorkflow implements INodeType { displayName: 'Call n8n Workflow Tool', name: 'toolWorkflow', icon: 'fa:network-wired', + iconColor: 'black', group: ['transform'], version: [1, 1.1, 1.2, 1.3], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.ts index dc99db630d..0323478ee8 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.ts @@ -26,6 +26,7 @@ export class VectorStoreInMemory extends createVectorStoreNode({ name: 'vectorStoreInMemory', description: 'Work with your data in In-Memory Vector Store', icon: 'fa:database', + iconColor: 'black', docsUrl: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreinmemory/', }, diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts index 6e684ebed3..711425df55 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.ts @@ -56,7 +56,7 @@ export class VectorStorePinecone extends createVectorStoreNode({ displayName: 'Pinecone Vector Store', name: 'vectorStorePinecone', description: 'Work with your data in Pinecone Vector Store', - icon: 'file:pinecone.svg', + icon: { light: 'file:pinecone.svg', dark: 'file:pinecone.dark.svg' }, docsUrl: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstorepinecone/', credentials: [ diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.dark.svg b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.dark.svg new file mode 100644 index 0000000000..4d163c6784 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.dark.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.svg b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.svg index b94b8b3af6..e9884a4249 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.svg +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/VectorStorePinecone/pinecone.svg @@ -1 +1,21 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts index 6d4abfb0cd..84f1d550e5 100644 --- a/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts +++ b/packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts @@ -17,6 +17,7 @@ import type { INodeListSearchResult, Icon, INodePropertyOptions, + ThemeIconColor, } from 'n8n-workflow'; import { getMetadataFiltersValues, logAiEvent } from '@utils/helpers'; @@ -37,6 +38,7 @@ interface NodeMeta { description: string; docsUrl: string; icon: Icon; + iconColor?: ThemeIconColor; credentials?: INodeCredentialDescription[]; operationModes?: NodeOperationMode[]; } @@ -125,6 +127,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) => name: args.meta.name, description: args.meta.description, icon: args.meta.icon, + iconColor: args.meta.iconColor, group: ['transform'], version: 1, defaults: { From 0ba4c6e9429a039845efb91f34428b3be4171483 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:19:06 +0200 Subject: [PATCH 15/25] test: Get rid of a warning log in tests (#12289) --- packages/cli/test/setup-test-folder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/test/setup-test-folder.ts b/packages/cli/test/setup-test-folder.ts index 997a0ec80f..8a58c48f86 100644 --- a/packages/cli/test/setup-test-folder.ts +++ b/packages/cli/test/setup-test-folder.ts @@ -10,6 +10,7 @@ mkdirSync(baseDir, { recursive: true }); const testDir = mkdtempSync(baseDir); mkdirSync(join(testDir, '.n8n')); process.env.N8N_USER_FOLDER = testDir; +process.env.N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS = 'false'; writeFileSync( join(testDir, '.n8n/config'), From 2e90eba47eff81f8b17a305cbc1656f929d622f8 Mon Sep 17 00:00:00 2001 From: jeanpaul Date: Thu, 19 Dec 2024 15:40:06 +0100 Subject: [PATCH 16/25] fix(OpenAI Node): Add quotes to default base URL (#12312) --- packages/nodes-base/nodes/OpenAi/OpenAi.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/OpenAi/OpenAi.node.ts b/packages/nodes-base/nodes/OpenAi/OpenAi.node.ts index a936337136..7e57a33db7 100644 --- a/packages/nodes-base/nodes/OpenAi/OpenAi.node.ts +++ b/packages/nodes-base/nodes/OpenAi/OpenAi.node.ts @@ -29,7 +29,7 @@ export class OpenAi implements INodeType { requestDefaults: { ignoreHttpStatusErrors: true, baseURL: - '={{ $credentials.url?.split("/").slice(0,-1).join("/") || https://api.openai.com }}', + '={{ $credentials.url?.split("/").slice(0,-1).join("/") ?? "https://api.openai.com" }}', }, properties: [ oldVersionNotice, From 8c635993bd65c84707938d9564d54c1ae17f1c1f Mon Sep 17 00:00:00 2001 From: oleg Date: Thu, 19 Dec 2024 17:42:52 +0100 Subject: [PATCH 17/25] fix(editor): Unify disabled parameters background color (#12306) --- .../InlineExpressionEditorInput.vue | 15 +-------------- .../editor-ui/src/components/ParameterInput.vue | 1 + .../src/components/ParameterInputList.vue | 1 + .../WorkflowSelectorParameterInput.vue | 1 + 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue index 593a0aa028..23cb31c7ea 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorInput.vue @@ -114,27 +114,14 @@ defineExpose({ - -