mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-27 13:39:44 -08:00
fix(core): Fix binary data helpers (like prepareBinaryData
) with task runner (#12259)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
92af245d1a
commit
0f1461f2d5
|
@ -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:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<unknown>) | 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
@ -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<IBinaryData>
|
||||
'helpers.assertBinaryData',
|
||||
|
||||
// getBinaryDataBuffer(itemIndex: number, propertyName: string): Promise<Buffer>
|
||||
'helpers.getBinaryDataBuffer',
|
||||
|
||||
// prepareBinaryData(binaryData: Buffer, fileName?: string, mimeType?: string): Promise<IBinaryData>
|
||||
'helpers.prepareBinaryData',
|
||||
|
||||
// setBinaryDataBuffer(metadata: IBinaryData, buffer: Buffer): Promise<IBinaryData>
|
||||
'helpers.setBinaryDataBuffer',
|
||||
|
||||
// binaryToString(body: Buffer, encoding?: string): string
|
||||
'helpers.binaryToString',
|
||||
|
||||
// httpRequest(opts: IHttpRequestOptions): Promise<IN8nHttpFullResponse | IN8nHttpResponse>
|
||||
'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 };
|
||||
|
|
|
@ -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<unknown>) | 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();
|
||||
|
|
|
@ -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<NodeTypes>();
|
||||
|
||||
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<Task>({
|
||||
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<Task>({
|
||||
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<Task>({
|
||||
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<Task>({
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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({
|
||||
|
|
24
packages/core/src/SerializedBuffer.ts
Normal file
24
packages/core/src/SerializedBuffer.ts
Normal file
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -24,3 +24,4 @@ export * from './ExecutionMetadata';
|
|||
export * from './node-execution-context';
|
||||
export * from './PartialExecutionUtils';
|
||||
export { ErrorReporter } from './error-reporter';
|
||||
export * from './SerializedBuffer';
|
||||
|
|
55
packages/core/test/SerializedBuffer.test.ts
Normal file
55
packages/core/test/SerializedBuffer.test.ts
Normal file
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
118
pnpm-lock.yaml
118
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
|
||||
|
|
Loading…
Reference in a new issue