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