Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: n8n-io/n8n
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: d9c67a72b22a282bf5522fa606c8fe192673ca0e
Choose a base ref
..
head repository: n8n-io/n8n
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 7b34a4f88a86439dcc8ac246ff0e772c906bfbfb
Choose a head ref
Showing with 379 additions and 565 deletions.
  1. +0 −1 packages/@n8n/nodes-langchain/.eslintrc.js
  2. +4 −2 packages/@n8n/nodes-langchain/nodes/code/Code.node.ts
  3. +1 −1 ...odes-langchain/nodes/document_loaders/DocumentDefaultDataLoader/DocumentDefaultDataLoader.node.ts
  4. +7 −5 ...des-langchain/nodes/output_parser/OutputParserAutofixing/test/OutputParserAutofixing.node.test.ts
  5. +2 −2 ...n/nodes-langchain/nodes/output_parser/OutputParserItemList/test/OutputParserItemList.node.test.ts
  6. +2 −2 ...des-langchain/nodes/output_parser/OutputParserStructured/test/OutputParserStructured.node.test.ts
  7. +3 −3 packages/@n8n/nodes-langchain/nodes/tools/ToolHttpRequest/test/ToolHttpRequest.node.test.ts
  8. +2 −2 packages/@n8n/nodes-langchain/nodes/vector_store/shared/createVectorStoreNode.ts
  9. +9 −10 packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts
  10. +4 −4 packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts
  11. +104 −133 packages/@n8n/nodes-langchain/utils/N8nTool.test.ts
  12. +1 −1 packages/@n8n/nodes-langchain/utils/N8nTool.ts
  13. +4 −38 packages/@n8n/nodes-langchain/utils/logWrapper.ts
  14. +2 −2 packages/@n8n/nodes-langchain/utils/tests/helpers.test.ts
  15. +4 −28 packages/core/src/NodeExecuteFunctions.ts
  16. +177 −279 packages/core/src/node-execution-context/__tests__/ai-root-node-context.test.ts
  17. +1 −3 packages/core/src/node-execution-context/__tests__/supply-data-context.test.ts
  18. +0 −22 packages/core/src/node-execution-context/base-execute-context.ts
  19. +0 −11 packages/core/src/node-execution-context/execute-context.ts
  20. +41 −0 packages/core/src/node-execution-context/node-execution-context.ts
  21. +11 −16 packages/workflow/src/Interfaces.ts
1 change: 0 additions & 1 deletion packages/@n8n/nodes-langchain/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ module.exports = {
eqeqeq: 'warn',
'id-denylist': 'warn',
'import/extensions': 'warn',
'import/order': 'warn',
'prefer-spread': 'warn',

'@typescript-eslint/naming-convention': ['error', { selector: 'memberLike', format: null }],
6 changes: 4 additions & 2 deletions packages/@n8n/nodes-langchain/nodes/code/Code.node.ts
Original file line number Diff line number Diff line change
@@ -77,8 +77,10 @@ function getSandbox(
const workflowMode = this.getMode();

const context = getSandboxContext.call(this, itemIndex);
context.addInputData = this.addInputData.bind(this);
context.addOutputData = this.addOutputData.bind(this);
if ('addInputData' in this) {
context.addInputData = this.addInputData.bind(this);
context.addOutputData = this.addOutputData.bind(this);
}

const { aiRootNodeContext } = this;
context.getInputConnectionData = async function (
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import {

import { logWrapper } from '@utils/logWrapper';
import { N8nBinaryLoader } from '@utils/N8nBinaryLoader';
import { N8nJsonLoader } from '@utils/N8nJsonLoader';
import { metadataFilterField } from '@utils/sharedFields';

// Dependencies needed underneath the hood for the loaders. We add them
@@ -17,7 +18,6 @@ import { metadataFilterField } from '@utils/sharedFields';
import 'mammoth'; // for docx
import 'epub2'; // for epub
import 'pdf-parse'; // for pdf
import { N8nJsonLoader } from '@utils/N8nJsonLoader';

export class DocumentDefaultDataLoader implements INodeType {
description: INodeTypeDescription = {
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { OutputParserException } from '@langchain/core/output_parsers';
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import { normalizeItems } from 'n8n-core';
import type { AiRootNodeFunctions, IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow';
import type {
AiRootNodeFunctions,
ISupplyDataFunctions,
IWorkflowDataProxyData,
} from 'n8n-workflow';
import { ApplicationError, NodeOperationError } from 'n8n-workflow';

import type {
@@ -18,14 +20,14 @@ import { NAIVE_FIX_PROMPT } from '../prompt';

describe('OutputParserAutofixing', () => {
let outputParser: OutputParserAutofixing;
let thisArg: MockProxy<IExecuteFunctions>;
let thisArg: MockProxy<ISupplyDataFunctions>;
let mockModel: MockProxy<BaseLanguageModel>;
let mockStructuredOutputParser: MockProxy<N8nStructuredOutputParser>;
const aiRootNodeContext = mock<AiRootNodeFunctions>();

beforeEach(() => {
outputParser = new OutputParserAutofixing();
thisArg = mock<IExecuteFunctions>({
thisArg = mock<ISupplyDataFunctions>({
aiRootNodeContext,
helpers: { normalizeItems },
});
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { mock } from 'jest-mock-extended';
import { normalizeItems } from 'n8n-core';
import {
ApplicationError,
type IExecuteFunctions,
type ISupplyDataFunctions,
type IWorkflowDataProxyData,
} from 'n8n-workflow';

@@ -12,7 +12,7 @@ import { OutputParserItemList } from '../OutputParserItemList.node';

describe('OutputParserItemList', () => {
let outputParser: OutputParserItemList;
const thisArg = mock<IExecuteFunctions>({
const thisArg = mock<ISupplyDataFunctions>({
helpers: { normalizeItems },
});
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { mock } from 'jest-mock-extended';
import { normalizeItems } from 'n8n-core';
import {
jsonParse,
type IExecuteFunctions,
type ISupplyDataFunctions,
type INode,
type IWorkflowDataProxyData,
} from 'n8n-workflow';
@@ -13,7 +13,7 @@ import { OutputParserStructured } from '../OutputParserStructured.node';

describe('OutputParserStructured', () => {
let outputParser: OutputParserStructured;
const thisArg = mock<IExecuteFunctions>({
const thisArg = mock<ISupplyDataFunctions>({
helpers: { normalizeItems },
});
const workflowDataProxy = mock<IWorkflowDataProxyData>({ $input: mock() });
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import type { ISupplyDataFunctions, INode } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';

import type { N8nTool } from '@utils/N8nTool';
@@ -8,8 +8,8 @@ import { ToolHttpRequest } from '../ToolHttpRequest.node';

describe('ToolHttpRequest', () => {
const httpTool = new ToolHttpRequest();
const helpers = mock<IExecuteFunctions['helpers']>();
const executeFunctions = mock<IExecuteFunctions>({ helpers });
const helpers = mock<ISupplyDataFunctions['helpers']>();
const executeFunctions = mock<ISupplyDataFunctions>({ helpers });

beforeEach(() => {
jest.resetAllMocks();
Original file line number Diff line number Diff line change
@@ -59,13 +59,13 @@ interface VectorStoreNodeConstructorArgs {
retrieveFields?: INodeProperties[];
updateFields?: INodeProperties[];
populateVectorStore: (
context: ISupplyDataFunctions,
context: IExecuteFunctions | ISupplyDataFunctions,
embeddings: Embeddings,
documents: Array<Document<Record<string, unknown>>>,
itemIndex: number,
) => Promise<void>;
getVectorStoreClient: (
context: ISupplyDataFunctions,
context: IExecuteFunctions | ISupplyDataFunctions,
filter: Record<string, never> | undefined,
embeddings: Embeddings,
itemIndex: number,
19 changes: 9 additions & 10 deletions packages/@n8n/nodes-langchain/utils/N8nBinaryLoader.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { pipeline } from 'stream/promises';
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv';
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
import { EPubLoader } from '@langchain/community/document_loaders/fs/epub';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import type { Document } from '@langchain/core/documents';
import type { TextSplitter } from '@langchain/textsplitters';
import { createWriteStream } from 'fs';
import { JSONLoader } from 'langchain/document_loaders/fs/json';
import { TextLoader } from 'langchain/document_loaders/fs/text';
import type {
IBinaryData,
IExecuteFunctions,
INodeExecutionData,
ISupplyDataFunctions,
} from 'n8n-workflow';
import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow';

import type { TextSplitter } from '@langchain/textsplitters';
import type { Document } from '@langchain/core/documents';
import { CSVLoader } from '@langchain/community/document_loaders/fs/csv';
import { DocxLoader } from '@langchain/community/document_loaders/fs/docx';
import { JSONLoader } from 'langchain/document_loaders/fs/json';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { TextLoader } from 'langchain/document_loaders/fs/text';
import { EPubLoader } from '@langchain/community/document_loaders/fs/epub';
import { pipeline } from 'stream/promises';
import { file as tmpFile, type DirectoryResult } from 'tmp-promise';

import { getMetadataFiltersValues } from './helpers';
8 changes: 4 additions & 4 deletions packages/@n8n/nodes-langchain/utils/N8nJsonLoader.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Document } from '@langchain/core/documents';
import type { TextSplitter } from '@langchain/textsplitters';
import { JSONLoader } from 'langchain/document_loaders/fs/json';
import { TextLoader } from 'langchain/document_loaders/fs/text';
import {
type IExecuteFunctions,
type INodeExecutionData,
type ISupplyDataFunctions,
NodeOperationError,
} from 'n8n-workflow';

import type { TextSplitter } from '@langchain/textsplitters';
import type { Document } from '@langchain/core/documents';
import { JSONLoader } from 'langchain/document_loaders/fs/json';
import { TextLoader } from 'langchain/document_loaders/fs/text';
import { getMetadataFiltersValues } from './helpers';

export class N8nJsonLoader {
237 changes: 104 additions & 133 deletions packages/@n8n/nodes-langchain/utils/N8nTool.test.ts
Original file line number Diff line number Diff line change
@@ -1,169 +1,140 @@
import { N8nTool } from './N8nTool';
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
import { z } from 'zod';
import type { INode } from 'n8n-workflow';
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
import { mock } from 'jest-mock-extended';
import type { ISupplyDataFunctions } from 'n8n-workflow';
import { z } from 'zod';

const mockNode: INode = {
id: '1',
name: 'Mock node',
typeVersion: 2,
type: 'n8n-nodes-base.mock',
position: [60, 760],
parameters: {
operation: 'test',
},
};

describe('Test N8nTool wrapper as DynamicStructuredTool', () => {
it('should wrap a tool', () => {
const func = jest.fn();

const ctx = createMockExecuteFunction({}, mockNode);

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
}),
});
import { N8nTool } from './N8nTool';

expect(tool).toBeInstanceOf(DynamicStructuredTool);
});
});
describe('N8nTool', () => {
const func = jest.fn();
const ctx = mock<ISupplyDataFunctions>();

describe('Test N8nTool wrapper - DynamicTool fallback', () => {
it('should convert the tool to a dynamic tool', () => {
const func = jest.fn();
beforeEach(() => jest.clearAllMocks());

const ctx = createMockExecuteFunction({}, mockNode);
describe('Test N8nTool wrapper as DynamicStructuredTool', () => {
it('should wrap a tool', () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
}),
});

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
}),
expect(tool).toBeInstanceOf(DynamicStructuredTool);
});

const dynamicTool = tool.asDynamicTool();

expect(dynamicTool).toBeInstanceOf(DynamicTool);
});

it('should format fallback description correctly', () => {
const func = jest.fn();
describe('Test N8nTool wrapper - DynamicTool fallback', () => {
it('should convert the tool to a dynamic tool', () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
}),
});

const ctx = createMockExecuteFunction({}, mockNode);
const dynamicTool = tool.asDynamicTool();

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
bar: z.number().optional(),
qwe: z.boolean().describe('Boolean description'),
}),
expect(dynamicTool).toBeInstanceOf(DynamicTool);
});

const dynamicTool = tool.asDynamicTool();

expect(dynamicTool.description).toContain('foo: (description: , type: string, required: true)');
expect(dynamicTool.description).toContain(
'bar: (description: , type: number, required: false)',
);

expect(dynamicTool.description).toContain(
'qwe: (description: Boolean description, type: boolean, required: true)',
);
});
it('should format fallback description correctly', () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string(),
bar: z.number().optional(),
qwe: z.boolean().describe('Boolean description'),
}),
});

const dynamicTool = tool.asDynamicTool();

expect(dynamicTool.description).toContain(
'foo: (description: , type: string, required: true)',
);
expect(dynamicTool.description).toContain(
'bar: (description: , type: number, required: false)',
);

expect(dynamicTool.description).toContain(
'qwe: (description: Boolean description, type: boolean, required: true)',
);
});

it('should handle empty parameter list correctly', () => {
const func = jest.fn();
it('should handle empty parameter list correctly', () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({}),
});

const ctx = createMockExecuteFunction({}, mockNode);
const dynamicTool = tool.asDynamicTool();

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({}),
expect(dynamicTool.description).toEqual('A dummy tool for testing');
});

const dynamicTool = tool.asDynamicTool();
it('should parse correct parameters', async () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
bar: z.number().optional(),
}),
});

expect(dynamicTool.description).toEqual('A dummy tool for testing');
});
const dynamicTool = tool.asDynamicTool();

it('should parse correct parameters', async () => {
const func = jest.fn();
const testParameters = { foo: 'some value' };

const ctx = createMockExecuteFunction({}, mockNode);
await dynamicTool.func(JSON.stringify(testParameters));

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
bar: z.number().optional(),
}),
expect(func).toHaveBeenCalledWith(testParameters);
});

const dynamicTool = tool.asDynamicTool();

const testParameters = { foo: 'some value' };
it('should recover when 1 parameter is passed directly', async () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
}),
});

await dynamicTool.func(JSON.stringify(testParameters));
const dynamicTool = tool.asDynamicTool();

expect(func).toHaveBeenCalledWith(testParameters);
});

it('should recover when 1 parameter is passed directly', async () => {
const func = jest.fn();
const testParameter = 'some value';

const ctx = createMockExecuteFunction({}, mockNode);
await dynamicTool.func(testParameter);

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
}),
expect(func).toHaveBeenCalledWith({ foo: testParameter });
});

const dynamicTool = tool.asDynamicTool();

const testParameter = 'some value';
it('should recover when JS object is passed instead of JSON', async () => {
const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
}),
});

await dynamicTool.func(testParameter);
const dynamicTool = tool.asDynamicTool();

expect(func).toHaveBeenCalledWith({ foo: testParameter });
});

it('should recover when JS object is passed instead of JSON', async () => {
const func = jest.fn();

const ctx = createMockExecuteFunction({}, mockNode);
await dynamicTool.func('{ foo: "some value" }');

const tool = new N8nTool(ctx, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func,
schema: z.object({
foo: z.string().describe('Foo description'),
}),
expect(func).toHaveBeenCalledWith({ foo: 'some value' });
});

const dynamicTool = tool.asDynamicTool();

await dynamicTool.func('{ foo: "some value" }');

expect(func).toHaveBeenCalledWith({ foo: 'some value' });
});
});
2 changes: 1 addition & 1 deletion packages/@n8n/nodes-langchain/utils/N8nTool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { DynamicStructuredToolInput } from '@langchain/core/tools';
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
import { StructuredOutputParser } from 'langchain/output_parsers';
import type { ISupplyDataFunctions, IDataObject, ZodObjectAny } from 'n8n-workflow';
import { NodeConnectionType, jsonParse, NodeOperationError } from 'n8n-workflow';
import { StructuredOutputParser } from 'langchain/output_parsers';
import type { ZodTypeAny } from 'zod';
import { ZodBoolean, ZodNullable, ZodNumber, ZodObject, ZodOptional } from 'zod';

42 changes: 4 additions & 38 deletions packages/@n8n/nodes-langchain/utils/logWrapper.ts
Original file line number Diff line number Diff line change
@@ -10,22 +10,17 @@ import type { Tool } from '@langchain/core/tools';
import { VectorStore } from '@langchain/core/vectorstores';
import { TextSplitter } from '@langchain/textsplitters';
import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base';
import type {
IExecuteFunctions,
INodeExecutionData,
ISupplyDataFunctions,
ITaskMetadata,
} from 'n8n-workflow';
import type { INodeExecutionData, ISupplyDataFunctions, ITaskMetadata } from 'n8n-workflow';
import { NodeOperationError, NodeConnectionType } from 'n8n-workflow';

import { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers';
import { N8nBinaryLoader } from './N8nBinaryLoader';
import { N8nJsonLoader } from './N8nJsonLoader';

export async function callMethodAsync<T>(
async function callMethodAsync<T>(
this: T,
parameters: {
executeFunctions: IExecuteFunctions | ISupplyDataFunctions;
executeFunctions: ISupplyDataFunctions;
connectionType: NodeConnectionType;
currentNodeRunIndex: number;
method: (...args: any[]) => Promise<unknown>;
@@ -62,35 +57,6 @@ export async function callMethodAsync<T>(
}
}

export function callMethodSync<T>(
this: T,
parameters: {
executeFunctions: IExecuteFunctions;
connectionType: NodeConnectionType;
currentNodeRunIndex: number;
method: (...args: any[]) => T;
arguments: unknown[];
},
): unknown {
try {
return parameters.method.call(this, ...parameters.arguments);
} catch (e) {
const connectedNode = parameters.executeFunctions.getNode();
const error = new NodeOperationError(connectedNode, e);
parameters.executeFunctions.addOutputData(
parameters.connectionType,
parameters.currentNodeRunIndex,
error,
);

throw new NodeOperationError(
connectedNode,
`Error on node "${connectedNode.name}" which is connected via input "${parameters.connectionType}"`,
{ functionality: 'configuration-node' },
);
}
}

export function logWrapper(
originalInstance:
| Tool
@@ -105,7 +71,7 @@ export function logWrapper(
| VectorStore
| N8nBinaryLoader
| N8nJsonLoader,
executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
executeFunctions: ISupplyDataFunctions,
) {
return new Proxy(originalInstance, {
get: (target, prop) => {
4 changes: 2 additions & 2 deletions packages/@n8n/nodes-langchain/utils/tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DynamicTool, type Tool } from '@langchain/core/tools';
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
import { NodeOperationError } from 'n8n-workflow';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import type { IExecuteFunctions, INode, ISupplyDataFunctions } from 'n8n-workflow';
import { z } from 'zod';

import { escapeSingleCurlyBrackets, getConnectedTools } from '../helpers';
@@ -171,7 +171,7 @@ describe('getConnectedTools', () => {

mockExecuteFunctions = createMockExecuteFunction({}, mockNode);

mockN8nTool = new N8nTool(mockExecuteFunctions, {
mockN8nTool = new N8nTool(mockExecuteFunctions as unknown as ISupplyDataFunctions, {
name: 'Dummy Tool',
description: 'A dummy tool for testing',
func: jest.fn(),
32 changes: 4 additions & 28 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
@@ -46,7 +46,6 @@ import type {
IN8nHttpResponse,
INode,
INodeExecutionData,
INodeInputConfiguration,
IOAuth2Options,
IPairedItemData,
IPollFunctions,
@@ -77,8 +76,6 @@ import type {
SchedulingFunctions,
SupplyData,
AINodeConnectionType,
IWebhookFunctions,
ISupplyDataFunctions,
} from 'n8n-workflow';
import {
NodeConnectionType,
@@ -119,6 +116,7 @@ import { createNodeAsTool } from './CreateNodeAsTool';
import { DataDeduplicationService } from './data-deduplication-service';
import { InstanceSettings } from './InstanceSettings';
import type { IResponseError } from './Interfaces';
import type { ExecuteContext, WebhookContext } from './node-execution-context';
// eslint-disable-next-line import/no-cycle
import { PollContext, SupplyDataContext, TriggerContext } from './node-execution-context';
import { ScheduledTaskManager } from './ScheduledTaskManager';
@@ -2011,7 +2009,7 @@ export function getWebhookDescription(
}

export async function getInputConnectionData(
this: IExecuteFunctions | IWebhookFunctions | ISupplyDataFunctions,
this: ExecuteContext | WebhookContext | SupplyDataContext,
workflow: Workflow,
runExecutionData: IRunExecutionData,
parentRunIndex: number,
@@ -2026,37 +2024,15 @@ export async function getInputConnectionData(
abortSignal?: AbortSignal,
): Promise<unknown> {
const parentNode = this.getNode();
const parentNodeType = workflow.nodeTypes.getByNameAndVersion(
parentNode.type,
parentNode.typeVersion,
);

const inputs = NodeHelpers.getNodeInputs(workflow, parentNode, parentNodeType.description);

let inputConfiguration = inputs.find((input) => {
if (typeof input === 'string') {
return input === connectionType;
}
return input.type === connectionType;
});

const inputConfiguration = this.nodeInputs.find((input) => input.type === connectionType);
if (inputConfiguration === undefined) {
throw new ApplicationError('Node does not have input of type', {
extra: { nodeName: parentNode.name, connectionType },
});
}

if (typeof inputConfiguration === 'string') {
inputConfiguration = {
type: inputConfiguration,
} as INodeInputConfiguration;
}

const connectedNodes = workflow
.getParentNodes(parentNode.name, connectionType, 1)
.map((nodeName) => workflow.getNode(nodeName) as INode)
.filter((connectedNode) => connectedNode.disabled !== true);

const connectedNodes = this.getConnectedNodes(connectionType);
if (connectedNodes.length === 0) {
if (inputConfiguration.required) {
throw new NodeOperationError(

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ import type {
INodeType,
INodeTypes,
ICredentialDataDecryptedObject,
AiRootNodeFunctions,
} from 'n8n-workflow';
import { ApplicationError, NodeConnectionType } from 'n8n-workflow';

@@ -64,9 +63,8 @@ describe('SupplyDataContext', () => {
const closeFn = jest.fn();
const abortSignal = mock<AbortSignal>();

const aiRootNodeContext = mock<AiRootNodeFunctions>();
const supplyDataContext = new SupplyDataContext(
aiRootNodeContext,
mock(),
workflow,
node,
additionalData,
22 changes: 0 additions & 22 deletions packages/core/src/node-execution-context/base-execute-context.ts
Original file line number Diff line number Diff line change
@@ -16,8 +16,6 @@ import type {
ITaskMetadata,
ContextType,
IContextObject,
INodeInputConfiguration,
INodeOutputConfiguration,
IWorkflowDataProxyData,
ISourceData,
AiEvent,
@@ -161,26 +159,6 @@ export class BaseExecuteContext extends NodeExecutionContext {
return allItems;
}

getNodeInputs(): INodeInputConfiguration[] {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
return NodeHelpers.getNodeInputs(this.workflow, this.node, nodeType.description).map((input) =>
typeof input === 'string' ? { type: input } : input,
);
}

getNodeOutputs(): INodeOutputConfiguration[] {
const nodeType = this.workflow.nodeTypes.getByNameAndVersion(
this.node.type,
this.node.typeVersion,
);
return NodeHelpers.getNodeOutputs(this.workflow, this.node, nodeType.description).map(
(output) => (typeof output === 'string' ? { type: output } : output),
);
}

getInputSourceData(inputIndex = 0, connectionType = NodeConnectionType.Main): ISourceData {
if (this.executeData?.source === null) {
// Should never happen as n8n sets it automatically
11 changes: 0 additions & 11 deletions packages/core/src/node-execution-context/execute-context.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@ import type {
WorkflowExecuteMode,
} from 'n8n-workflow';
import {
ApplicationError,
createDeferredPromise,
createEnvProviderState,
NodeConnectionType,
@@ -196,16 +195,6 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti
await this.additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
}

/** @deprecated use ISupplyDataFunctions.addInputData */
addInputData(): { index: number } {
throw new ApplicationError('addInputData should not be called on IExecuteFunctions');
}

/** @deprecated use ISupplyDataFunctions.addOutputData */
addOutputData(): void {
throw new ApplicationError('addOutputData should not be called on IExecuteFunctions');
}

getParentCallbackManager(): CallbackManager | undefined {
return this.additionalData.parentCallbackManager;
}
41 changes: 41 additions & 0 deletions packages/core/src/node-execution-context/node-execution-context.ts
Original file line number Diff line number Diff line change
@@ -9,8 +9,11 @@ import type {
INodeCredentialDescription,
INodeCredentialsDetails,
INodeExecutionData,
INodeInputConfiguration,
INodeOutputConfiguration,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
NodeConnectionType,
NodeParameterValueType,
NodeTypeAndVersion,
Workflow,
@@ -27,6 +30,7 @@ import {
import { Container } from 'typedi';

import { HTTP_REQUEST_NODE_TYPE, HTTP_REQUEST_TOOL_NODE_TYPE } from '@/Constants';
import { Memoized } from '@/decorators';
import { extractValue } from '@/ExtractValue';
import { InstanceSettings } from '@/InstanceSettings';

@@ -108,6 +112,42 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
return output;
}

@Memoized
get nodeType() {
const { type, typeVersion } = this.node;
return this.workflow.nodeTypes.getByNameAndVersion(type, typeVersion);
}

@Memoized
get nodeInputs() {
return NodeHelpers.getNodeInputs(this.workflow, this.node, this.nodeType.description).map(
(input) => (typeof input === 'string' ? { type: input } : input),
);
}

getNodeInputs(): INodeInputConfiguration[] {
return this.nodeInputs;
}

@Memoized
get nodeOutputs() {
return NodeHelpers.getNodeOutputs(this.workflow, this.node, this.nodeType.description).map(
(output) => (typeof output === 'string' ? { type: output } : output),
);
}

getConnectedNodes(connectionType: NodeConnectionType): INode[] {
return this.workflow
.getParentNodes(this.node.name, connectionType, 1)
.map((nodeName) => this.workflow.getNode(nodeName))
.filter((node) => !!node)
.filter((node) => node.disabled !== true);
}

getNodeOutputs(): INodeOutputConfiguration[] {
return this.nodeOutputs;
}

getKnownNodeTypes() {
return this.workflow.nodeTypes.getKnownTypes();
}
@@ -260,6 +300,7 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
return decryptedDataObject as T;
}

@Memoized
protected get additionalKeys() {
return getAdditionalKeys(this.additionalData, this.mode, this.runExecutionData);
}
27 changes: 11 additions & 16 deletions packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -932,19 +932,6 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
sendMessageToUI(message: any): void;
sendResponse(response: IExecuteResponsePromiseData): void;

// TODO: move this to ISupplyDataFunctions
addInputData(
connectionType: NodeConnectionType,
data: INodeExecutionData[][] | ExecutionError,
runIndex?: number,
): { index: number };
addOutputData(
connectionType: NodeConnectionType,
currentNodeRunIndex: number,
data: INodeExecutionData[][] | ExecutionError,
metadata?: ITaskMetadata,
): void;

nodeHelpers: NodeHelperFunctions;
helpers: RequestHelperFunctions &
BaseHelperFunctions &
@@ -975,7 +962,6 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &

export type OutputParserType = ZodType<object, ZodTypeDef, object>;
export type AiRootNodeFunctions = {
rootContext: IExecuteFunctions | IWebhookFunctions;
getModel(itemIndex?: number): Promise<BaseLanguageModel>;
getMemory(itemIndex?: number): Promise<BaseChatMemory>;
getRetriever(itemIndex?: number): Promise<BaseRetriever>;
@@ -1008,8 +994,6 @@ export type ISupplyDataFunctions = ExecuteFunctions.GetNodeParameterFn &
FunctionsBaseWithRequiredKeys<'getMode'> &
Pick<
IExecuteFunctions,
| 'addInputData'
| 'addOutputData'
| 'continueOnFail'
| 'evaluateExpression'
| 'executeWorkflow'
@@ -1024,6 +1008,17 @@ export type ISupplyDataFunctions = ExecuteFunctions.GetNodeParameterFn &
| 'helpers'
> & {
aiRootNodeContext: AiRootNodeFunctions;
addInputData(
connectionType: NodeConnectionType,
data: INodeExecutionData[][] | ExecutionError,
runIndex?: number,
): { index: number };
addOutputData(
connectionType: NodeConnectionType,
currentNodeRunIndex: number,
data: INodeExecutionData[][] | ExecutionError,
metadata?: ITaskMetadata,
): void;
};

export interface IExecutePaginationFunctions extends IExecuteSingleFunctions {