Merge remote-tracking branch 'origin/master' into PAY-1681-move-curlconverter-to-frontend

This commit is contained in:
Csaba Tuncsik 2024-10-31 11:22:21 +01:00
commit 63d9c84bc3
No known key found for this signature in database
158 changed files with 2924 additions and 828 deletions

View file

@ -7,6 +7,7 @@
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mjmlio.vscode-mjml", "mjmlio.vscode-mjml",
"Vue.volar" "Vue.volar",
"vitest.explorer"
] ]
} }

View file

@ -68,7 +68,7 @@ If you already have VS Code and Docker installed, you can click [here](https://v
#### Node.js #### Node.js
[Node.js](https://nodejs.org/en/) version 18.10 or newer is required for development purposes. [Node.js](https://nodejs.org/en/) version 20.15 or newer is required for development purposes.
#### pnpm #### pnpm

View file

@ -0,0 +1,21 @@
import { WEBHOOK_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2270 Save button resets on webhook node open', () => {
it('should not reset the save button if webhook node is opened and closed', () => {
workflowPage.actions.visit();
workflowPage.actions.addInitialNodeToCanvas(WEBHOOK_NODE_NAME);
workflowPage.getters.saveButton().click();
workflowPage.actions.openNode(WEBHOOK_NODE_NAME);
ndv.actions.close();
cy.ifCanvasVersion(
() => cy.getByTestId('workflow-save-button').should('not.contain', 'Saved'),
() => cy.getByTestId('workflow-save-button').should('contain', 'Saved'),
);
});
});

View file

@ -10,8 +10,10 @@
"N8N_RUNNERS_GRANT_TOKEN", "N8N_RUNNERS_GRANT_TOKEN",
"N8N_RUNNERS_N8N_URI", "N8N_RUNNERS_N8N_URI",
"N8N_RUNNERS_MAX_PAYLOAD", "N8N_RUNNERS_MAX_PAYLOAD",
"N8N_RUNNERS_MAX_CONCURRENCY",
"NODE_FUNCTION_ALLOW_BUILTIN", "NODE_FUNCTION_ALLOW_BUILTIN",
"NODE_FUNCTION_ALLOW_EXTERNAL" "NODE_FUNCTION_ALLOW_EXTERNAL",
"NODE_OPTIONS"
], ],
"uid": 2000, "uid": 2000,
"gid": 2000 "gid": 2000

View file

@ -21,6 +21,7 @@
"dist/**/*" "dist/**/*"
], ],
"devDependencies": { "devDependencies": {
"@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*" "n8n-workflow": "workspace:*"
}, },
"dependencies": { "dependencies": {

View file

@ -1,3 +1,4 @@
import type { FrontendBetaFeatures } from '@n8n/config';
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow'; import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
export interface IVersionNotificationSettings { export interface IVersionNotificationSettings {
@ -169,4 +170,5 @@ export interface FrontendSettings {
security: { security: {
blockFileAccessToN8nFiles: boolean; blockFileAccessToN8nFiles: boolean;
}; };
betaFeatures: FrontendBetaFeatures[];
} }

View file

@ -0,0 +1,11 @@
import { Config, Env } from '../decorators';
import { StringArray } from '../utils';
export type FrontendBetaFeatures = 'canvas_v2';
@Config
export class FrontendConfig {
/** Which UI experiments to enable. Separate multiple values with a comma `,` */
@Env('N8N_UI_BETA_FEATURES')
betaFeatures: StringArray<FrontendBetaFeatures> = [];
}

View file

@ -0,0 +1,28 @@
import { Config, Env } from '../decorators';
@Config
export class LicenseConfig {
/** License server URL to retrieve license. */
@Env('N8N_LICENSE_SERVER_URL')
serverUrl: string = 'https://license.n8n.io/v1';
/** Whether autorenewal for licenses is enabled. */
@Env('N8N_LICENSE_AUTO_RENEW_ENABLED')
autoRenewalEnabled: boolean = true;
/** How long (in seconds) before expiry a license should be autorenewed. */
@Env('N8N_LICENSE_AUTO_RENEW_OFFSET')
autoRenewOffset: number = 60 * 60 * 72; // 72 hours
/** Activation key to initialize license. */
@Env('N8N_LICENSE_ACTIVATION_KEY')
activationKey: string = '';
/** Tenant ID used by the license manager SDK, e.g. for self-hosted, sandbox, embed, cloud. */
@Env('N8N_LICENSE_TENANT_ID')
tenantId: number = 1;
/** Ephemeral license certificate. See: https://github.com/n8n-io/license-management?tab=readme-ov-file#concept-ephemeral-entitlements */
@Env('N8N_LICENSE_CERT')
cert: string = '';
}

View file

@ -11,6 +11,7 @@ export const LOG_SCOPES = [
'redis', 'redis',
'scaling', 'scaling',
'waiting-executions', 'waiting-executions',
'task-runner',
] as const; ] as const;
export type LogScope = (typeof LOG_SCOPES)[number]; export type LogScope = (typeof LOG_SCOPES)[number];

View file

@ -42,4 +42,12 @@ export class TaskRunnersConfig {
/** Which task runner to launch from the config */ /** Which task runner to launch from the config */
@Env('N8N_RUNNERS_LAUNCHER_RUNNER') @Env('N8N_RUNNERS_LAUNCHER_RUNNER')
launcherRunner: string = 'javascript'; launcherRunner: string = 'javascript';
/** The --max-old-space-size option to use for the runner (in MB). Default means node.js will determine it based on the available memory. */
@Env('N8N_RUNNERS_MAX_OLD_SPACE_SIZE')
maxOldSpaceSize: string = '';
/** How many concurrent tasks can a runner execute at a time */
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
maxConcurrency: number = 5;
} }

View file

@ -0,0 +1,27 @@
import { Config, Env } from '../decorators';
@Config
export class SecurityConfig {
/**
* Which directories to limit n8n's access to. Separate multiple dirs with semicolon `;`.
*
* @example N8N_RESTRICT_FILE_ACCESS_TO=/home/user/.n8n;/home/user/n8n-data
*/
@Env('N8N_RESTRICT_FILE_ACCESS_TO')
restrictFileAccessTo: string = '';
/**
* Whether to block access to all files at:
* - the ".n8n" directory,
* - the static cache dir at ~/.cache/n8n/public, and
* - user-defined config files.
*/
@Env('N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES')
blockFileAccessToN8nFiles: boolean = true;
/**
* In a [security audit](https://docs.n8n.io/hosting/securing/security-audit/), how many days for a workflow to be considered abandoned if not executed.
*/
@Env('N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW')
daysAbandonedWorkflow: number = 90;
}

View file

@ -6,21 +6,25 @@ import { EventBusConfig } from './configs/event-bus.config';
import { ExternalSecretsConfig } from './configs/external-secrets.config'; import { ExternalSecretsConfig } from './configs/external-secrets.config';
import { ExternalStorageConfig } from './configs/external-storage.config'; import { ExternalStorageConfig } from './configs/external-storage.config';
import { GenericConfig } from './configs/generic.config'; import { GenericConfig } from './configs/generic.config';
import { LicenseConfig } from './configs/license.config';
import { LoggingConfig } from './configs/logging.config'; import { LoggingConfig } from './configs/logging.config';
import { MultiMainSetupConfig } from './configs/multi-main-setup.config'; import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
import { NodesConfig } from './configs/nodes.config'; import { NodesConfig } from './configs/nodes.config';
import { PublicApiConfig } from './configs/public-api.config'; import { PublicApiConfig } from './configs/public-api.config';
import { TaskRunnersConfig } from './configs/runners.config'; import { TaskRunnersConfig } from './configs/runners.config';
export { TaskRunnersConfig } from './configs/runners.config';
import { ScalingModeConfig } from './configs/scaling-mode.config'; import { ScalingModeConfig } from './configs/scaling-mode.config';
import { SecurityConfig } from './configs/security.config';
import { SentryConfig } from './configs/sentry.config'; import { SentryConfig } from './configs/sentry.config';
import { TemplatesConfig } from './configs/templates.config'; import { TemplatesConfig } from './configs/templates.config';
import { UserManagementConfig } from './configs/user-management.config'; import { UserManagementConfig } from './configs/user-management.config';
import { VersionNotificationsConfig } from './configs/version-notifications.config'; import { VersionNotificationsConfig } from './configs/version-notifications.config';
import { WorkflowsConfig } from './configs/workflows.config'; import { WorkflowsConfig } from './configs/workflows.config';
import { Config, Env, Nested } from './decorators'; import { Config, Env, Nested } from './decorators';
export { Config, Env, Nested } from './decorators';
export { Config, Env, Nested } from './decorators';
export { TaskRunnersConfig } from './configs/runners.config';
export { SecurityConfig } from './configs/security.config';
export { FrontendBetaFeatures, FrontendConfig } from './configs/frontend.config';
export { LOG_SCOPES } from './configs/logging.config'; export { LOG_SCOPES } from './configs/logging.config';
export type { LogScope } from './configs/logging.config'; export type { LogScope } from './configs/logging.config';
@ -102,4 +106,10 @@ export class GlobalConfig {
@Nested @Nested
generic: GenericConfig; generic: GenericConfig;
@Nested
license: LicenseConfig;
@Nested
security: SecurityConfig;
} }

View file

@ -231,6 +231,8 @@ describe('GlobalConfig', () => {
port: 5679, port: 5679,
launcherPath: '', launcherPath: '',
launcherRunner: 'javascript', launcherRunner: 'javascript',
maxOldSpaceSize: '',
maxConcurrency: 5,
}, },
sentry: { sentry: {
backendDsn: '', backendDsn: '',
@ -256,6 +258,19 @@ describe('GlobalConfig', () => {
releaseChannel: 'dev', releaseChannel: 'dev',
gracefulShutdownTimeout: 30, gracefulShutdownTimeout: 30,
}, },
license: {
serverUrl: 'https://license.n8n.io/v1',
autoRenewalEnabled: true,
autoRenewOffset: 60 * 60 * 72,
activationKey: '',
tenantId: 1,
cert: '',
},
security: {
restrictFileAccessTo: '',
blockFileAccessToN8nFiles: true,
daysAbandonedWorkflow: 90,
},
}; };
it('should use all default values when no env variables are defined', () => { it('should use all default values when no env variables are defined', () => {

View file

@ -1,13 +1,13 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import { NodeOperationError, NodeConnectionType } from 'n8n-workflow';
NodeOperationError, import type {
type IExecuteFunctions, IExecuteFunctions,
type INodeExecutionData, INodeExecutionData,
type INodeType, INodeType,
type INodeTypeDescription, INodeTypeDescription,
type INodeOutputConfiguration, INodeOutputConfiguration,
type SupplyData, SupplyData,
NodeConnectionType, ISupplyDataFunctions,
} from 'n8n-workflow'; } from 'n8n-workflow';
// TODO: Add support for execute function. Got already started but got commented out // TODO: Add support for execute function. Got already started but got commented out
@ -72,7 +72,7 @@ export const vmResolver = makeResolverFromLegacyOptions({
}); });
function getSandbox( function getSandbox(
this: IExecuteFunctions, this: IExecuteFunctions | ISupplyDataFunctions,
code: string, code: string,
options?: { addItems?: boolean; itemIndex?: number }, options?: { addItems?: boolean; itemIndex?: number },
) { ) {
@ -354,7 +354,7 @@ export class Code implements INodeType {
} }
} }
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const code = this.getNodeParameter('code', itemIndex) as { supplyData?: { code: string } }; const code = this.getNodeParameter('code', itemIndex) as { supplyData?: { code: string } };
if (!code.supplyData?.code) { if (!code.supplyData?.code) {

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -177,7 +177,7 @@ export class DocumentBinaryInputLoader implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
this.logger.debug('Supply Data for Binary Input Loader'); this.logger.debug('Supply Data for Binary Input Loader');
const textSplitter = (await this.getInputConnectionData( const textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter, NodeConnectionType.AiTextSplitter,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -283,7 +283,7 @@ export class DocumentDefaultDataLoader implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const dataType = this.getNodeParameter('dataType', itemIndex, 'json') as 'json' | 'binary'; const dataType = this.getNodeParameter('dataType', itemIndex, 'json') as 'json' | 'binary';
const textSplitter = (await this.getInputConnectionData( const textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter, NodeConnectionType.AiTextSplitter,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { GithubRepoLoader } from '@langchain/community/document_loaders/web/github'; import { GithubRepoLoader } from '@langchain/community/document_loaders/web/github';
@ -93,7 +93,7 @@ export class DocumentGithubLoader implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
console.log('Supplying data for Github Document Loader'); console.log('Supplying data for Github Document Loader');
const repository = this.getNodeParameter('repository', itemIndex) as string; const repository = this.getNodeParameter('repository', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -79,7 +79,7 @@ export class DocumentJsonInputLoader implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
this.logger.debug('Supply Data for JSON Input Loader'); this.logger.debug('Supply Data for JSON Input Loader');
const textSplitter = (await this.getInputConnectionData( const textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter, NodeConnectionType.AiTextSplitter,

View file

@ -2,9 +2,9 @@
import { BedrockEmbeddings } from '@langchain/aws'; import { BedrockEmbeddings } from '@langchain/aws';
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -104,7 +104,7 @@ export class EmbeddingsAwsBedrock implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('aws'); const credentials = await this.getCredentials('aws');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -92,7 +92,7 @@ export class EmbeddingsAzureOpenAi implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings'); this.logger.debug('Supply data for embeddings');
const credentials = await this.getCredentials<{ const credentials = await this.getCredentials<{
apiKey: string; apiKey: string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CohereEmbeddings } from '@langchain/cohere'; import { CohereEmbeddings } from '@langchain/cohere';
@ -99,7 +99,7 @@ export class EmbeddingsCohere implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings Cohere'); this.logger.debug('Supply data for embeddings Cohere');
const modelName = this.getNodeParameter('modelName', itemIndex, 'embed-english-v2.0') as string; const modelName = this.getNodeParameter('modelName', itemIndex, 'embed-english-v2.0') as string;
const credentials = await this.getCredentials<{ apiKey: string }>('cohereApi'); const credentials = await this.getCredentials<{ apiKey: string }>('cohereApi');

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai'; import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
@ -116,7 +116,7 @@ export class EmbeddingsGoogleGemini implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings Google Gemini'); this.logger.debug('Supply data for embeddings Google Gemini');
const modelName = this.getNodeParameter( const modelName = this.getNodeParameter(
'modelName', 'modelName',

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { HuggingFaceInferenceEmbeddings } from '@langchain/community/embeddings/hf'; import { HuggingFaceInferenceEmbeddings } from '@langchain/community/embeddings/hf';
@ -81,7 +81,7 @@ export class EmbeddingsHuggingFaceInference implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings HF Inference'); this.logger.debug('Supply data for embeddings HF Inference');
const model = this.getNodeParameter( const model = this.getNodeParameter(
'modelName', 'modelName',

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { MistralAIEmbeddingsParams } from '@langchain/mistralai'; import type { MistralAIEmbeddingsParams } from '@langchain/mistralai';
@ -134,7 +134,7 @@ export class EmbeddingsMistralCloud implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('mistralCloudApi'); const credentials = await this.getCredentials('mistralCloudApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter( const options = this.getNodeParameter(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { OllamaEmbeddings } from '@langchain/ollama'; import { OllamaEmbeddings } from '@langchain/ollama';
@ -44,7 +44,7 @@ export class EmbeddingsOllama implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiVectorStore]), ollamaModel], properties: [getConnectionHintNoticeField([NodeConnectionType.AiVectorStore]), ollamaModel],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings Ollama'); this.logger.debug('Supply data for embeddings Ollama');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;
const credentials = await this.getCredentials('ollamaApi'); const credentials = await this.getCredentials('ollamaApi');

View file

@ -1,10 +1,10 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type SupplyData, type SupplyData,
type ISupplyDataFunctions,
type INodeProperties, type INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -170,7 +170,7 @@ export class EmbeddingsOpenAi implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply data for embeddings'); this.logger.debug('Supply data for embeddings');
const credentials = await this.getCredentials('openAiApi'); const credentials = await this.getCredentials('openAiApi');

View file

@ -3,7 +3,7 @@ import {
NodeConnectionType, NodeConnectionType,
type INodePropertyOptions, type INodePropertyOptions,
type INodeProperties, type INodeProperties,
type IExecuteFunctions, type ISupplyDataFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type SupplyData, type SupplyData,
@ -20,6 +20,10 @@ const modelField: INodeProperties = {
type: 'options', type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [ options: [
{
name: 'Claude 3.5 Sonnet(20241022)',
value: 'claude-3-5-sonnet-20241022',
},
{ {
name: 'Claude 3 Opus(20240229)', name: 'Claude 3 Opus(20240229)',
value: 'claude-3-opus-20240229', value: 'claude-3-opus-20240229',
@ -175,7 +179,7 @@ export class LmChatAnthropic implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('anthropicApi'); const credentials = await this.getCredentials('anthropicApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -52,7 +52,7 @@ export class LmChatOllama implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('ollamaApi'); const credentials = await this.getCredentials('ollamaApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
type JsonObject, type JsonObject,
NodeApiError, NodeApiError,
@ -242,7 +242,7 @@ export class LmChatOpenAi implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('openAiApi'); const credentials = await this.getCredentials('openAiApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -90,7 +90,7 @@ export class LmCohere implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('cohereApi'); const credentials = await this.getCredentials('cohereApi');
const options = this.getNodeParameter('options', itemIndex, {}) as object; const options = this.getNodeParameter('options', itemIndex, {}) as object;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -51,7 +51,7 @@ export class LmOllama implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('ollamaApi'); const credentials = await this.getCredentials('ollamaApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import type { import type {
IExecuteFunctions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
ISupplyDataFunctions,
SupplyData, SupplyData,
ILoadOptionsFunctions, ILoadOptionsFunctions,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -229,7 +229,7 @@ export class LmOpenAi implements INodeType {
}, },
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('openAiApi'); const credentials = await this.getCredentials('openAiApi');
const modelName = this.getNodeParameter('model', itemIndex, '', { const modelName = this.getNodeParameter('model', itemIndex, '', {

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -132,7 +132,7 @@ export class LmOpenHuggingFaceInference implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('huggingFaceApi'); const credentials = await this.getCredentials('huggingFaceApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -2,9 +2,9 @@
import { ChatBedrockConverse } from '@langchain/aws'; import { ChatBedrockConverse } from '@langchain/aws';
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -132,7 +132,7 @@ export class LmChatAwsBedrock implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('aws'); const credentials = await this.getCredentials('aws');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -162,7 +162,7 @@ export class LmChatAzureOpenAi implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials<{ const credentials = await this.getCredentials<{
apiKey: string; apiKey: string;
resourceName: string; resourceName: string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
@ -113,7 +113,7 @@ export class LmChatGoogleGemini implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('googlePalmApi'); const credentials = await this.getCredentials('googlePalmApi');
const modelName = this.getNodeParameter('modelName', itemIndex) as string; const modelName = this.getNodeParameter('modelName', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
type ILoadOptionsFunctions, type ILoadOptionsFunctions,
type JsonObject, type JsonObject,
@ -124,7 +124,7 @@ export class LmChatGoogleVertex implements INodeType {
}, },
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('googleApi'); const credentials = await this.getCredentials('googleApi');
const privateKey = formatPrivateKey(credentials.privateKey as string); const privateKey = formatPrivateKey(credentials.privateKey as string);
const email = (credentials.email as string).trim(); const email = (credentials.email as string).trim();

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -129,7 +129,7 @@ export class LmChatGroq implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('groqApi'); const credentials = await this.getCredentials('groqApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -172,7 +172,7 @@ export class LmChatMistralCloud implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('mistralCloudApi'); const credentials = await this.getCredentials('mistralCloudApi');
const modelName = this.getNodeParameter('model', itemIndex) as string; const modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -7,7 +7,7 @@ import type {
SerializedSecret, SerializedSecret,
} from '@langchain/core/load/serializable'; } from '@langchain/core/load/serializable';
import type { LLMResult } from '@langchain/core/outputs'; import type { LLMResult } from '@langchain/core/outputs';
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; import type { IDataObject, ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { pick } from 'lodash'; import { pick } from 'lodash';
import type { BaseMessage } from '@langchain/core/messages'; import type { BaseMessage } from '@langchain/core/messages';
@ -30,8 +30,6 @@ const TIKTOKEN_ESTIMATE_MODEL = 'gpt-4o';
export class N8nLlmTracing extends BaseCallbackHandler { export class N8nLlmTracing extends BaseCallbackHandler {
name = 'N8nLlmTracing'; name = 'N8nLlmTracing';
executionFunctions: IExecuteFunctions;
connectionType = NodeConnectionType.AiLanguageModel; connectionType = NodeConnectionType.AiLanguageModel;
promptTokensEstimate = 0; promptTokensEstimate = 0;
@ -61,11 +59,10 @@ export class N8nLlmTracing extends BaseCallbackHandler {
}; };
constructor( constructor(
executionFunctions: IExecuteFunctions, private executionFunctions: ISupplyDataFunctions,
options?: { tokensUsageParser: TokensUsageParser }, options?: { tokensUsageParser: TokensUsageParser },
) { ) {
super(); super();
this.executionFunctions = executionFunctions;
this.options = { ...this.options, ...options }; this.options = { ...this.options, ...options };
} }
@ -138,7 +135,7 @@ export class N8nLlmTracing extends BaseCallbackHandler {
this.executionFunctions.addOutputData(this.connectionType, runDetails.index, [ this.executionFunctions.addOutputData(this.connectionType, runDetails.index, [
[{ json: { ...response } }], [{ json: { ...response } }],
]); ]);
void logAiEvent(this.executionFunctions, 'ai-llm-generated-output', { logAiEvent(this.executionFunctions, 'ai-llm-generated-output', {
messages: parsedMessages, messages: parsedMessages,
options: runDetails.options, options: runDetails.options,
response, response,
@ -186,7 +183,7 @@ export class N8nLlmTracing extends BaseCallbackHandler {
}); });
} }
void logAiEvent(this.executionFunctions, 'ai-llm-errored', { logAiEvent(this.executionFunctions, 'ai-llm-errored', {
error: Object.keys(error).length === 0 ? error.toString() : error, error: Object.keys(error).length === 0 ? error.toString() : error,
runId, runId,
parentRunId, parentRunId,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { BufferWindowMemoryInput } from 'langchain/memory'; import type { BufferWindowMemoryInput } from 'langchain/memory';
@ -134,7 +134,7 @@ export class MemoryBufferWindow implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const contextWindowLength = this.getNodeParameter('contextWindowLength', itemIndex) as number; const contextWindowLength = this.getNodeParameter('contextWindowLength', itemIndex) as number;
const workflowId = this.getWorkflow().id; const workflowId = this.getWorkflow().id;
const memoryInstance = MemoryChatBufferSingleton.getInstance(); const memoryInstance = MemoryChatBufferSingleton.getInstance();

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -86,7 +86,7 @@ export class MemoryMotorhead implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('motorheadApi'); const credentials = await this.getCredentials('motorheadApi');
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;

View file

@ -1,5 +1,10 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow'; import type {
ISupplyDataFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { BufferMemory, BufferWindowMemory } from 'langchain/memory'; import { BufferMemory, BufferWindowMemory } from 'langchain/memory';
import { PostgresChatMessageHistory } from '@langchain/community/stores/message/postgres'; import { PostgresChatMessageHistory } from '@langchain/community/stores/message/postgres';
@ -73,7 +78,7 @@ export class MemoryPostgresChat implements INodeType {
}, },
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials<PostgresNodeCredentials>('postgres'); const credentials = await this.getCredentials<PostgresNodeCredentials>('postgres');
const tableName = this.getNodeParameter('tableName', itemIndex, 'n8n_chat_histories') as string; const tableName = this.getNodeParameter('tableName', itemIndex, 'n8n_chat_histories') as string;
const sessionId = getSessionId(this, itemIndex); const sessionId = getSessionId(this, itemIndex);

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeOperationError, NodeOperationError,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
NodeConnectionType, NodeConnectionType,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -102,7 +102,7 @@ export class MemoryRedisChat implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('redis'); const credentials = await this.getCredentials('redis');
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;

View file

@ -1,6 +1,11 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow'; import type {
ISupplyDataFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
} from 'n8n-workflow';
import { XataChatMessageHistory } from '@langchain/community/stores/message/xata'; import { XataChatMessageHistory } from '@langchain/community/stores/message/xata';
import { BufferMemory, BufferWindowMemory } from 'langchain/memory'; import { BufferMemory, BufferWindowMemory } from 'langchain/memory';
import { BaseClient } from '@xata.io/client'; import { BaseClient } from '@xata.io/client';
@ -88,7 +93,7 @@ export class MemoryXata implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('xataApi'); const credentials = await this.getCredentials('xataApi');
const nodeVersion = this.getNode().typeVersion; const nodeVersion = this.getNode().typeVersion;

View file

@ -1,7 +1,7 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions, type ISupplyDataFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type SupplyData, type SupplyData,
@ -103,7 +103,7 @@ export class MemoryZep implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials<{ const credentials = await this.getCredentials<{
apiKey?: string; apiKey?: string;
apiUrl?: string; apiUrl?: string;

View file

@ -1,6 +1,11 @@
import type { BaseLanguageModel } from '@langchain/core/language_models/base'; import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow'; import type {
ISupplyDataFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
} from 'n8n-workflow';
import { import {
N8nOutputFixingParser, N8nOutputFixingParser,
@ -63,7 +68,7 @@ export class OutputParserAutofixing implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const model = (await this.getInputConnectionData( const model = (await this.getInputConnectionData(
NodeConnectionType.AiLanguageModel, NodeConnectionType.AiLanguageModel,
itemIndex, itemIndex,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -80,7 +80,7 @@ export class OutputParserItemList implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const options = this.getNodeParameter('options', itemIndex, {}) as { const options = this.getNodeParameter('options', itemIndex, {}) as {
numberOfItems?: number; numberOfItems?: number;
separator?: string; separator?: string;

View file

@ -1,9 +1,9 @@
import type { JSONSchema7 } from 'json-schema'; import type { JSONSchema7 } from 'json-schema';
import { import {
jsonParse, jsonParse,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
NodeOperationError, NodeOperationError,
NodeConnectionType, NodeConnectionType,
@ -122,7 +122,7 @@ export class OutputParserStructured implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const schemaType = this.getNodeParameter('schemaType', itemIndex, '') as 'fromJson' | 'manual'; const schemaType = this.getNodeParameter('schemaType', itemIndex, '') as 'fromJson' | 'manual';
// We initialize these even though one of them will always be empty // We initialize these even though one of them will always be empty
// it makes it easer to navigate the ternary operator // it makes it easer to navigate the ternary operator

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -63,7 +63,7 @@ export class RetrieverContextualCompression implements INodeType {
properties: [], properties: [],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supplying data for Contextual Compression Retriever'); this.logger.debug('Supplying data for Contextual Compression Retriever');
const model = (await this.getInputConnectionData( const model = (await this.getInputConnectionData(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -82,7 +82,7 @@ export class RetrieverMultiQuery implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supplying data for MultiQuery Retriever'); this.logger.debug('Supplying data for MultiQuery Retriever');
const options = this.getNodeParameter('options', itemIndex, {}) as { queryCount?: number }; const options = this.getNodeParameter('options', itemIndex, {}) as { queryCount?: number };

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { VectorStore } from '@langchain/core/vectorstores'; import type { VectorStore } from '@langchain/core/vectorstores';
@ -56,7 +56,7 @@ export class RetrieverVectorStore implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supplying data for Vector Store Retriever'); this.logger.debug('Supplying data for Vector Store Retriever');
const topK = this.getNodeParameter('topK', itemIndex, 4) as number; const topK = this.getNodeParameter('topK', itemIndex, 4) as number;

View file

@ -5,7 +5,7 @@ import type {
IExecuteWorkflowInfo, IExecuteWorkflowInfo,
INodeExecutionData, INodeExecutionData,
IWorkflowBase, IWorkflowBase,
IExecuteFunctions, ISupplyDataFunctions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
SupplyData, SupplyData,
@ -292,15 +292,15 @@ export class RetrieverWorkflow implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
class WorkflowRetriever extends BaseRetriever { class WorkflowRetriever extends BaseRetriever {
lc_namespace = ['n8n-nodes-langchain', 'retrievers', 'workflow']; lc_namespace = ['n8n-nodes-langchain', 'retrievers', 'workflow'];
executeFunctions: IExecuteFunctions; constructor(
private executeFunctions: ISupplyDataFunctions,
constructor(executeFunctions: IExecuteFunctions, fields: BaseRetrieverInput) { fields: BaseRetrieverInput,
) {
super(fields); super(fields);
this.executeFunctions = executeFunctions;
} }
async _getRelevantDocuments( async _getRelevantDocuments(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { CharacterTextSplitterParams } from '@langchain/textsplitters'; import type { CharacterTextSplitterParams } from '@langchain/textsplitters';
@ -63,7 +63,7 @@ export class TextSplitterCharacterTextSplitter implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply Data for Text Splitter'); this.logger.debug('Supply Data for Text Splitter');
const separator = this.getNodeParameter('separator', itemIndex) as string; const separator = this.getNodeParameter('separator', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { import type {
@ -94,7 +94,7 @@ export class TextSplitterRecursiveCharacterTextSplitter implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply Data for Text Splitter'); this.logger.debug('Supply Data for Text Splitter');
const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number; const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { TokenTextSplitter } from '@langchain/textsplitters'; import { TokenTextSplitter } from '@langchain/textsplitters';
@ -56,7 +56,7 @@ export class TextSplitterTokenSplitter implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply Data for Text Splitter'); this.logger.debug('Supply Data for Text Splitter');
const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number; const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Calculator } from '@langchain/community/tools/calculator'; import { Calculator } from '@langchain/community/tools/calculator';
@ -43,7 +43,7 @@ export class ToolCalculator implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])], properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])],
}; };
async supplyData(this: IExecuteFunctions): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
return { return {
response: logWrapper(new Calculator(), this), response: logWrapper(new Calculator(), this),
}; };

View file

@ -6,9 +6,9 @@ import { PythonSandbox } from 'n8n-nodes-base/dist/nodes/Code/PythonSandbox';
import type { Sandbox } from 'n8n-nodes-base/dist/nodes/Code/Sandbox'; import type { Sandbox } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox'; import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
import type { import type {
IExecuteFunctions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
ISupplyDataFunctions,
SupplyData, SupplyData,
ExecutionError, ExecutionError,
IDataObject, IDataObject,
@ -175,7 +175,7 @@ export class ToolCode implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const node = this.getNode(); const node = this.getNode();
const workflowMode = this.getMode(); const workflowMode = this.getMode();

View file

@ -1,8 +1,8 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type { import type {
IExecuteFunctions,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
ISupplyDataFunctions,
SupplyData, SupplyData,
IHttpRequestMethods, IHttpRequestMethods,
IHttpRequestOptions, IHttpRequestOptions,
@ -250,7 +250,7 @@ export class ToolHttpRequest implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNode().name.replace(/ /g, '_'); const name = this.getNode().name.replace(/ /g, '_');
try { try {
tryToParseAlphanumericString(name); tryToParseAlphanumericString(name);

View file

@ -10,19 +10,19 @@ describe('ToolHttpRequest', () => {
const helpers = mock<IExecuteFunctions['helpers']>(); const helpers = mock<IExecuteFunctions['helpers']>();
const executeFunctions = mock<IExecuteFunctions>({ helpers }); const executeFunctions = mock<IExecuteFunctions>({ helpers });
describe('Binary response', () => { beforeEach(() => {
beforeEach(() => { jest.resetAllMocks();
jest.resetAllMocks(); executeFunctions.getNode.mockReturnValue(
executeFunctions.getNode.mockReturnValue( mock<INode>({
mock<INode>({ type: 'n8n-nodes-base.httpRequest',
type: 'n8n-nodes-base.httpRequest', name: 'HTTP Request',
name: 'HTTP Request', typeVersion: 1.1,
typeVersion: 1.1, }),
}), );
); executeFunctions.addInputData.mockReturnValue({ index: 0 });
executeFunctions.addInputData.mockReturnValue({ index: 0 }); });
});
describe('Binary response', () => {
it('should return the error when receiving a binary response', async () => { it('should return the error when receiving a binary response', async () => {
helpers.httpRequest.mockResolvedValue({ helpers.httpRequest.mockResolvedValue({
body: Buffer.from(''), body: Buffer.from(''),
@ -237,4 +237,62 @@ describe('ToolHttpRequest', () => {
); );
}); });
}); });
describe('Optimize response', () => {
it('should extract body from the response HTML', async () => {
helpers.httpRequest.mockResolvedValue({
body: `<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Test</h1>
<div>
<p>
Test content
</p>
</div>
</body>
</html>`,
headers: {
'content-type': 'text/html',
},
});
executeFunctions.getNodeParameter.mockImplementation(
(paramName: string, _: any, fallback: any) => {
switch (paramName) {
case 'method':
return 'GET';
case 'url':
return '{url}';
case 'options':
return {};
case 'placeholderDefinitions.values':
return [];
case 'optimizeResponse':
return true;
case 'responseType':
return 'html';
case 'cssSelector':
return 'body';
default:
return fallback;
}
},
);
const { response } = await httpTool.supplyData.call(executeFunctions, 0);
const res = await (response as N8nTool).invoke({
url: 'https://httpbin.org/html',
});
expect(helpers.httpRequest).toHaveBeenCalled();
expect(res).toEqual(
JSON.stringify(['<h1>Test</h1> <div> <p> Test content </p> </div>'], null, 2),
);
});
});
}); });

View file

@ -1,5 +1,5 @@
import { Readability } from '@mozilla/readability'; import { Readability } from '@mozilla/readability';
import cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { convert } from 'html-to-text'; import { convert } from 'html-to-text';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import get from 'lodash/get'; import get from 'lodash/get';
@ -8,12 +8,12 @@ import unset from 'lodash/unset';
import * as mime from 'mime-types'; import * as mime from 'mime-types';
import { getOAuth2AdditionalParameters } from 'n8n-nodes-base/dist/nodes/HttpRequest/GenericFunctions'; import { getOAuth2AdditionalParameters } from 'n8n-nodes-base/dist/nodes/HttpRequest/GenericFunctions';
import type { import type {
IExecuteFunctions,
IDataObject, IDataObject,
IHttpRequestOptions, IHttpRequestOptions,
IRequestOptionsSimplified, IRequestOptionsSimplified,
ExecutionError, ExecutionError,
NodeApiError, NodeApiError,
ISupplyDataFunctions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
import { z } from 'zod'; import { z } from 'zod';
@ -28,7 +28,7 @@ import type {
} from './interfaces'; } from './interfaces';
import type { DynamicZodObject } from '../../../types/zod.types'; import type { DynamicZodObject } from '../../../types/zod.types';
const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => { const genericCredentialRequest = async (ctx: ISupplyDataFunctions, itemIndex: number) => {
const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string; const genericType = ctx.getNodeParameter('genericAuthType', itemIndex) as string;
if (genericType === 'httpBasicAuth' || genericType === 'httpDigestAuth') { if (genericType === 'httpBasicAuth' || genericType === 'httpDigestAuth') {
@ -104,7 +104,7 @@ const genericCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: numbe
}); });
}; };
const predefinedCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: number) => { const predefinedCredentialRequest = async (ctx: ISupplyDataFunctions, itemIndex: number) => {
const predefinedType = ctx.getNodeParameter('nodeCredentialType', itemIndex) as string; const predefinedType = ctx.getNodeParameter('nodeCredentialType', itemIndex) as string;
const additionalOptions = getOAuth2AdditionalParameters(predefinedType); const additionalOptions = getOAuth2AdditionalParameters(predefinedType);
@ -119,7 +119,7 @@ const predefinedCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: nu
}; };
export const configureHttpRequestFunction = async ( export const configureHttpRequestFunction = async (
ctx: IExecuteFunctions, ctx: ISupplyDataFunctions,
credentialsType: 'predefinedCredentialType' | 'genericCredentialType' | 'none', credentialsType: 'predefinedCredentialType' | 'genericCredentialType' | 'none',
itemIndex: number, itemIndex: number,
) => { ) => {
@ -146,7 +146,7 @@ const defaultOptimizer = <T>(response: T) => {
return String(response); return String(response);
}; };
const htmlOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: number) => { const htmlOptimizer = (ctx: ISupplyDataFunctions, itemIndex: number, maxLength: number) => {
const cssSelector = ctx.getNodeParameter('cssSelector', itemIndex, '') as string; const cssSelector = ctx.getNodeParameter('cssSelector', itemIndex, '') as string;
const onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean; const onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean;
let elementsToOmit: string[] = []; let elementsToOmit: string[] = [];
@ -214,7 +214,7 @@ const htmlOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: num
}; };
}; };
const textOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: number) => { const textOptimizer = (ctx: ISupplyDataFunctions, itemIndex: number, maxLength: number) => {
return (response: string | IDataObject) => { return (response: string | IDataObject) => {
if (typeof response === 'object') { if (typeof response === 'object') {
try { try {
@ -245,7 +245,7 @@ const textOptimizer = (ctx: IExecuteFunctions, itemIndex: number, maxLength: num
}; };
}; };
const jsonOptimizer = (ctx: IExecuteFunctions, itemIndex: number) => { const jsonOptimizer = (ctx: ISupplyDataFunctions, itemIndex: number) => {
return (response: string): string => { return (response: string): string => {
let responseData: IDataObject | IDataObject[] | string = response; let responseData: IDataObject | IDataObject[] | string = response;
@ -324,7 +324,7 @@ const jsonOptimizer = (ctx: IExecuteFunctions, itemIndex: number) => {
}; };
}; };
export const configureResponseOptimizer = (ctx: IExecuteFunctions, itemIndex: number) => { export const configureResponseOptimizer = (ctx: ISupplyDataFunctions, itemIndex: number) => {
const optimizeResponse = ctx.getNodeParameter('optimizeResponse', itemIndex, false) as boolean; const optimizeResponse = ctx.getNodeParameter('optimizeResponse', itemIndex, false) as boolean;
if (optimizeResponse) { if (optimizeResponse) {
@ -469,7 +469,7 @@ const MODEL_INPUT_DESCRIPTION = {
}; };
export const updateParametersAndOptions = (options: { export const updateParametersAndOptions = (options: {
ctx: IExecuteFunctions; ctx: ISupplyDataFunctions;
itemIndex: number; itemIndex: number;
toolParameters: ToolParameter[]; toolParameters: ToolParameter[];
placeholdersDefinitions: PlaceholderDefinition[]; placeholdersDefinitions: PlaceholderDefinition[];
@ -558,7 +558,7 @@ export const prepareToolDescription = (
}; };
export const configureToolFunction = ( export const configureToolFunction = (
ctx: IExecuteFunctions, ctx: ISupplyDataFunctions,
itemIndex: number, itemIndex: number,
toolParameters: ToolParameter[], toolParameters: ToolParameter[],
requestOptions: IHttpRequestOptions, requestOptions: IHttpRequestOptions,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { SerpAPI } from '@langchain/community/tools/serpapi'; import { SerpAPI } from '@langchain/community/tools/serpapi';
@ -113,7 +113,7 @@ export class ToolSerpApi implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const credentials = await this.getCredentials('serpApi'); const credentials = await this.getCredentials('serpApi');
const options = this.getNodeParameter('options', itemIndex) as object; const options = this.getNodeParameter('options', itemIndex) as object;

View file

@ -1,4 +1,9 @@
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow'; import type {
INodeType,
INodeTypeDescription,
ISupplyDataFunctions,
SupplyData,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { VectorStoreQATool } from 'langchain/tools'; import { VectorStoreQATool } from 'langchain/tools';
@ -82,7 +87,7 @@ export class ToolVectorStore implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNodeParameter('name', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex) as string;
const toolDescription = this.getNodeParameter('description', itemIndex) as string; const toolDescription = this.getNodeParameter('description', itemIndex) as string;
const topK = this.getNodeParameter('topK', itemIndex, 4) as number; const topK = this.getNodeParameter('topK', itemIndex, 4) as number;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { WikipediaQueryRun } from '@langchain/community/tools/wikipedia_query_run'; import { WikipediaQueryRun } from '@langchain/community/tools/wikipedia_query_run';
@ -43,7 +43,7 @@ export class ToolWikipedia implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])], properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])],
}; };
async supplyData(this: IExecuteFunctions): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
const WikiTool = new WikipediaQueryRun(); const WikiTool = new WikipediaQueryRun();
WikiTool.description = WikiTool.description =

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { WolframAlphaTool } from '@langchain/community/tools/wolframalpha'; import { WolframAlphaTool } from '@langchain/community/tools/wolframalpha';
@ -49,7 +49,7 @@ export class ToolWolframAlpha implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])], properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])],
}; };
async supplyData(this: IExecuteFunctions): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
const credentials = await this.getCredentials('wolframAlphaApi'); const credentials = await this.getCredentials('wolframAlphaApi');
return { return {

View file

@ -6,12 +6,12 @@ import isObject from 'lodash/isObject';
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode'; import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
import type { import type {
IExecuteFunctions,
IExecuteWorkflowInfo, IExecuteWorkflowInfo,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IWorkflowBase, IWorkflowBase,
ISupplyDataFunctions,
SupplyData, SupplyData,
ExecutionError, ExecutionError,
IDataObject, IDataObject,
@ -357,7 +357,7 @@ export class ToolWorkflow implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNodeParameter('name', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex) as string;
const description = this.getNodeParameter('description', itemIndex) as string; const description = this.getNodeParameter('description', itemIndex) as string;

View file

@ -1,10 +1,10 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */ /* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { import {
NodeConnectionType, NodeConnectionType,
type SupplyData,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { Embeddings } from '@langchain/core/embeddings'; import type { Embeddings } from '@langchain/core/embeddings';
import { MemoryVectorStoreManager } from '../shared/MemoryVectorStoreManager'; import { MemoryVectorStoreManager } from '../shared/MemoryVectorStoreManager';
@ -59,7 +59,7 @@ export class VectorStoreInMemoryLoad implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const embeddings = (await this.getInputConnectionData( const embeddings = (await this.getInputConnectionData(
NodeConnectionType.AiEmbedding, NodeConnectionType.AiEmbedding,
itemIndex, itemIndex,

View file

@ -1,8 +1,8 @@
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { PineconeStoreParams } from '@langchain/pinecone'; import type { PineconeStoreParams } from '@langchain/pinecone';
@ -84,7 +84,7 @@ export class VectorStorePineconeLoad implements INodeType {
}, },
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supplying data for Pinecone Load Vector Store'); this.logger.debug('Supplying data for Pinecone Load Vector Store');
const namespace = this.getNodeParameter('pineconeNamespace', itemIndex) as string; const namespace = this.getNodeParameter('pineconeNamespace', itemIndex) as string;

View file

@ -1,7 +1,7 @@
import { import {
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
NodeConnectionType, NodeConnectionType,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -81,7 +81,7 @@ export class VectorStoreSupabaseLoad implements INodeType {
methods = { listSearch: { supabaseTableNameSearch } }; methods = { listSearch: { supabaseTableNameSearch } };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supply Supabase Load Vector Store'); this.logger.debug('Supply Supabase Load Vector Store');
const tableName = this.getNodeParameter('tableName', itemIndex, '', { const tableName = this.getNodeParameter('tableName', itemIndex, '', {

View file

@ -1,8 +1,8 @@
import { import {
NodeConnectionType, NodeConnectionType,
type IExecuteFunctions,
type INodeType, type INodeType,
type INodeTypeDescription, type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData, type SupplyData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { IZepConfig } from '@langchain/community/vectorstores/zep'; import type { IZepConfig } from '@langchain/community/vectorstores/zep';
@ -83,7 +83,7 @@ export class VectorStoreZepLoad implements INodeType {
], ],
}; };
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
this.logger.debug('Supplying data for Zep Load Vector Store'); this.logger.debug('Supplying data for Zep Load Vector Store');
const collectionName = this.getNodeParameter('collectionName', itemIndex) as string; const collectionName = this.getNodeParameter('collectionName', itemIndex) as string;

View file

@ -7,11 +7,8 @@ export class MemoryVectorStoreManager {
private vectorStoreBuffer: Map<string, MemoryVectorStore>; private vectorStoreBuffer: Map<string, MemoryVectorStore>;
private embeddings: Embeddings; private constructor(private embeddings: Embeddings) {
private constructor(embeddings: Embeddings) {
this.vectorStoreBuffer = new Map(); this.vectorStoreBuffer = new Map();
this.embeddings = embeddings;
} }
public static getInstance(embeddings: Embeddings): MemoryVectorStoreManager { public static getInstance(embeddings: Embeddings): MemoryVectorStoreManager {

View file

@ -5,12 +5,13 @@ import type { Embeddings } from '@langchain/core/embeddings';
import type { VectorStore } from '@langchain/core/vectorstores'; import type { VectorStore } from '@langchain/core/vectorstores';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type { import type {
IExecuteFunctions,
INodeCredentialDescription, INodeCredentialDescription,
INodeProperties, INodeProperties,
INodeExecutionData, INodeExecutionData,
IExecuteFunctions,
INodeTypeDescription, INodeTypeDescription,
SupplyData, SupplyData,
ISupplyDataFunctions,
INodeType, INodeType,
ILoadOptionsFunctions, ILoadOptionsFunctions,
INodeListSearchResult, INodeListSearchResult,
@ -57,13 +58,13 @@ interface VectorStoreNodeConstructorArgs {
retrieveFields?: INodeProperties[]; retrieveFields?: INodeProperties[];
updateFields?: INodeProperties[]; updateFields?: INodeProperties[];
populateVectorStore: ( populateVectorStore: (
context: IExecuteFunctions, context: ISupplyDataFunctions,
embeddings: Embeddings, embeddings: Embeddings,
documents: Array<Document<Record<string, unknown>>>, documents: Array<Document<Record<string, unknown>>>,
itemIndex: number, itemIndex: number,
) => Promise<void>; ) => Promise<void>;
getVectorStoreClient: ( getVectorStoreClient: (
context: IExecuteFunctions, context: ISupplyDataFunctions,
filter: Record<string, never> | undefined, filter: Record<string, never> | undefined,
embeddings: Embeddings, embeddings: Embeddings,
itemIndex: number, itemIndex: number,
@ -281,7 +282,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
}); });
resultData.push(...serializedDocs); resultData.push(...serializedDocs);
void logAiEvent(this, 'ai-vector-store-searched', { query: prompt }); logAiEvent(this, 'ai-vector-store-searched', { query: prompt });
} }
return [resultData]; return [resultData];
@ -311,7 +312,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
try { try {
await args.populateVectorStore(this, embeddings, processedDocuments, itemIndex); await args.populateVectorStore(this, embeddings, processedDocuments, itemIndex);
void logAiEvent(this, 'ai-vector-store-populated'); logAiEvent(this, 'ai-vector-store-populated');
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -365,7 +366,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
ids: [documentId], ids: [documentId],
}); });
void logAiEvent(this, 'ai-vector-store-updated'); logAiEvent(this, 'ai-vector-store-updated');
} catch (error) { } catch (error) {
throw error; throw error;
} }
@ -380,7 +381,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
); );
} }
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> { async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve'; const mode = this.getNodeParameter('mode', 0) as 'load' | 'insert' | 'retrieve';
const filter = getMetadataFiltersValues(this, itemIndex); const filter = getMetadataFiltersValues(this, itemIndex);
const embeddings = (await this.getInputConnectionData( const embeddings = (await this.getInputConnectionData(

View file

@ -1,6 +1,11 @@
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs'; import { createWriteStream } from 'fs';
import type { IBinaryData, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import type {
IBinaryData,
IExecuteFunctions,
INodeExecutionData,
ISupplyDataFunctions,
} from 'n8n-workflow';
import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow'; import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow';
import type { TextSplitter } from '@langchain/textsplitters'; import type { TextSplitter } from '@langchain/textsplitters';
@ -26,25 +31,12 @@ const SUPPORTED_MIME_TYPES = {
}; };
export class N8nBinaryLoader { export class N8nBinaryLoader {
private context: IExecuteFunctions;
private optionsPrefix: string;
private binaryDataKey: string;
private textSplitter?: TextSplitter;
constructor( constructor(
context: IExecuteFunctions, private context: IExecuteFunctions | ISupplyDataFunctions,
optionsPrefix = '', private optionsPrefix = '',
binaryDataKey = '', private binaryDataKey = '',
textSplitter?: TextSplitter, private textSplitter?: TextSplitter,
) { ) {}
this.context = context;
this.textSplitter = textSplitter;
this.optionsPrefix = optionsPrefix;
this.binaryDataKey = binaryDataKey;
}
async processAll(items?: INodeExecutionData[]): Promise<Document[]> { async processAll(items?: INodeExecutionData[]): Promise<Document[]> {
const docs: Document[] = []; const docs: Document[] = [];

View file

@ -1,4 +1,9 @@
import { type IExecuteFunctions, type INodeExecutionData, NodeOperationError } from 'n8n-workflow'; import {
type IExecuteFunctions,
type INodeExecutionData,
type ISupplyDataFunctions,
NodeOperationError,
} from 'n8n-workflow';
import type { TextSplitter } from '@langchain/textsplitters'; import type { TextSplitter } from '@langchain/textsplitters';
import type { Document } from '@langchain/core/documents'; import type { Document } from '@langchain/core/documents';
@ -7,17 +12,11 @@ import { TextLoader } from 'langchain/document_loaders/fs/text';
import { getMetadataFiltersValues } from './helpers'; import { getMetadataFiltersValues } from './helpers';
export class N8nJsonLoader { export class N8nJsonLoader {
private context: IExecuteFunctions; constructor(
private context: IExecuteFunctions | ISupplyDataFunctions,
private optionsPrefix: string; private optionsPrefix = '',
private textSplitter?: TextSplitter,
private textSplitter?: TextSplitter; ) {}
constructor(context: IExecuteFunctions, optionsPrefix = '', textSplitter?: TextSplitter) {
this.context = context;
this.textSplitter = textSplitter;
this.optionsPrefix = optionsPrefix;
}
async processAll(items?: INodeExecutionData[]): Promise<Document[]> { async processAll(items?: INodeExecutionData[]): Promise<Document[]> {
const docs: Document[] = []; const docs: Document[] = [];

View file

@ -1,6 +1,6 @@
import type { DynamicStructuredToolInput } from '@langchain/core/tools'; import type { DynamicStructuredToolInput } from '@langchain/core/tools';
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools'; import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
import type { IExecuteFunctions, IDataObject } from 'n8n-workflow'; import type { ISupplyDataFunctions, IDataObject } from 'n8n-workflow';
import { NodeConnectionType, jsonParse, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, jsonParse, NodeOperationError } from 'n8n-workflow';
import { StructuredOutputParser } from 'langchain/output_parsers'; import { StructuredOutputParser } from 'langchain/output_parsers';
import type { ZodTypeAny } from 'zod'; import type { ZodTypeAny } from 'zod';
@ -45,12 +45,11 @@ ALL parameters marked as required must be provided`;
}; };
export class N8nTool extends DynamicStructuredTool { export class N8nTool extends DynamicStructuredTool {
private context: IExecuteFunctions; constructor(
private context: ISupplyDataFunctions,
constructor(context: IExecuteFunctions, fields: DynamicStructuredToolInput) { fields: DynamicStructuredToolInput,
) {
super(fields); super(fields);
this.context = context;
} }
asDynamicTool(): DynamicTool { asDynamicTool(): DynamicTool {

View file

@ -5,7 +5,13 @@ import type { BaseMessage } from '@langchain/core/messages';
import type { Tool } from '@langchain/core/tools'; import type { Tool } from '@langchain/core/tools';
import type { BaseChatMemory } from 'langchain/memory'; import type { BaseChatMemory } from 'langchain/memory';
import { NodeConnectionType, NodeOperationError, jsonStringify } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError, jsonStringify } from 'n8n-workflow';
import type { AiEvent, IDataObject, IExecuteFunctions, IWebhookFunctions } from 'n8n-workflow'; import type {
AiEvent,
IDataObject,
IExecuteFunctions,
ISupplyDataFunctions,
IWebhookFunctions,
} from 'n8n-workflow';
import { N8nTool } from './N8nTool'; import { N8nTool } from './N8nTool';
@ -20,7 +26,7 @@ function hasMethods<T>(obj: unknown, ...methodNames: Array<string | symbol>): ob
} }
export function getMetadataFiltersValues( export function getMetadataFiltersValues(
ctx: IExecuteFunctions, ctx: IExecuteFunctions | ISupplyDataFunctions,
itemIndex: number, itemIndex: number,
): Record<string, never> | undefined { ): Record<string, never> | undefined {
const options = ctx.getNodeParameter('options', itemIndex, {}); const options = ctx.getNodeParameter('options', itemIndex, {});
@ -93,7 +99,7 @@ export function getPromptInputByType(options: {
} }
export function getSessionId( export function getSessionId(
ctx: IExecuteFunctions | IWebhookFunctions, ctx: ISupplyDataFunctions | IWebhookFunctions,
itemIndex: number, itemIndex: number,
selectorKey = 'sessionIdType', selectorKey = 'sessionIdType',
autoSelect = 'fromInput', autoSelect = 'fromInput',
@ -133,13 +139,13 @@ export function getSessionId(
return sessionId; return sessionId;
} }
export async function logAiEvent( export function logAiEvent(
executeFunctions: IExecuteFunctions, executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
event: AiEvent, event: AiEvent,
data?: IDataObject, data?: IDataObject,
) { ) {
try { try {
await executeFunctions.logAiEvent(event, data ? jsonStringify(data) : undefined); executeFunctions.logAiEvent(event, data ? jsonStringify(data) : undefined);
} catch (error) { } catch (error) {
executeFunctions.logger.debug(`Error logging AI event: ${event}`); executeFunctions.logger.debug(`Error logging AI event: ${event}`);
} }

View file

@ -10,7 +10,7 @@ import type { Tool } from '@langchain/core/tools';
import { VectorStore } from '@langchain/core/vectorstores'; import { VectorStore } from '@langchain/core/vectorstores';
import { TextSplitter } from '@langchain/textsplitters'; import { TextSplitter } from '@langchain/textsplitters';
import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base'; import type { BaseDocumentLoader } from 'langchain/dist/document_loaders/base';
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import type { IExecuteFunctions, INodeExecutionData, ISupplyDataFunctions } from 'n8n-workflow';
import { NodeOperationError, NodeConnectionType } from 'n8n-workflow'; import { NodeOperationError, NodeConnectionType } from 'n8n-workflow';
import { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers'; import { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers';
@ -27,7 +27,7 @@ const errorsMap: { [key: string]: { message: string; description: string } } = {
export async function callMethodAsync<T>( export async function callMethodAsync<T>(
this: T, this: T,
parameters: { parameters: {
executeFunctions: IExecuteFunctions; executeFunctions: IExecuteFunctions | ISupplyDataFunctions;
connectionType: NodeConnectionType; connectionType: NodeConnectionType;
currentNodeRunIndex: number; currentNodeRunIndex: number;
method: (...args: any[]) => Promise<unknown>; method: (...args: any[]) => Promise<unknown>;
@ -113,7 +113,7 @@ export function logWrapper(
| VectorStore | VectorStore
| N8nBinaryLoader | N8nBinaryLoader
| N8nJsonLoader, | N8nJsonLoader,
executeFunctions: IExecuteFunctions, executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
) { ) {
return new Proxy(originalInstance, { return new Proxy(originalInstance, {
get: (target, prop) => { get: (target, prop) => {
@ -190,7 +190,7 @@ export function logWrapper(
const payload = { action: 'getMessages', response }; const payload = { action: 'getMessages', response };
executeFunctions.addOutputData(connectionType, index, [[{ json: payload }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: payload }]]);
void logAiEvent(executeFunctions, 'ai-messages-retrieved-from-memory', { response }); logAiEvent(executeFunctions, 'ai-messages-retrieved-from-memory', { response });
return response; return response;
}; };
} else if (prop === 'addMessage' && 'addMessage' in target) { } else if (prop === 'addMessage' && 'addMessage' in target) {
@ -207,7 +207,7 @@ export function logWrapper(
arguments: [message], arguments: [message],
}); });
void logAiEvent(executeFunctions, 'ai-message-added-to-memory', { message }); logAiEvent(executeFunctions, 'ai-message-added-to-memory', { message });
executeFunctions.addOutputData(connectionType, index, [[{ json: payload }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: payload }]]);
}; };
} }
@ -233,7 +233,7 @@ export function logWrapper(
arguments: [query, config], arguments: [query, config],
})) as Array<Document<Record<string, any>>>; })) as Array<Document<Record<string, any>>>;
void logAiEvent(executeFunctions, 'ai-documents-retrieved', { query }); logAiEvent(executeFunctions, 'ai-documents-retrieved', { query });
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;
}; };
@ -258,7 +258,7 @@ export function logWrapper(
arguments: [documents], arguments: [documents],
})) as number[][]; })) as number[][];
void logAiEvent(executeFunctions, 'ai-document-embedded'); logAiEvent(executeFunctions, 'ai-document-embedded');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;
}; };
@ -278,7 +278,7 @@ export function logWrapper(
method: target[prop], method: target[prop],
arguments: [query], arguments: [query],
})) as number[]; })) as number[];
void logAiEvent(executeFunctions, 'ai-query-embedded'); logAiEvent(executeFunctions, 'ai-query-embedded');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;
}; };
@ -323,7 +323,7 @@ export function logWrapper(
arguments: [item, itemIndex], arguments: [item, itemIndex],
})) as number[]; })) as number[];
void logAiEvent(executeFunctions, 'ai-document-processed'); logAiEvent(executeFunctions, 'ai-document-processed');
executeFunctions.addOutputData(connectionType, index, [ executeFunctions.addOutputData(connectionType, index, [
[{ json: { response }, pairedItem: { item: itemIndex } }], [{ json: { response }, pairedItem: { item: itemIndex } }],
]); ]);
@ -349,7 +349,7 @@ export function logWrapper(
arguments: [text], arguments: [text],
})) as string[]; })) as string[];
void logAiEvent(executeFunctions, 'ai-text-split'); logAiEvent(executeFunctions, 'ai-text-split');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;
}; };
@ -373,7 +373,7 @@ export function logWrapper(
arguments: [query], arguments: [query],
})) as string; })) as string;
void logAiEvent(executeFunctions, 'ai-tool-called', { query, response }); logAiEvent(executeFunctions, 'ai-tool-called', { query, response });
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;
}; };
@ -403,7 +403,7 @@ export function logWrapper(
arguments: [query, k, filter, _callbacks], arguments: [query, k, filter, _callbacks],
})) as Array<Document<Record<string, any>>>; })) as Array<Document<Record<string, any>>>;
void logAiEvent(executeFunctions, 'ai-vector-store-searched', { query }); logAiEvent(executeFunctions, 'ai-vector-store-searched', { query });
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]); executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response; return response;

View file

@ -2,7 +2,7 @@ import type { Callbacks } from '@langchain/core/callbacks/manager';
import type { BaseLanguageModel } from '@langchain/core/language_models/base'; import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import type { AIMessage } from '@langchain/core/messages'; import type { AIMessage } from '@langchain/core/messages';
import { BaseOutputParser } from '@langchain/core/output_parsers'; import { BaseOutputParser } from '@langchain/core/output_parsers';
import type { IExecuteFunctions } from 'n8n-workflow'; import type { ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import type { N8nStructuredOutputParser } from './N8nStructuredOutputParser'; import type { N8nStructuredOutputParser } from './N8nStructuredOutputParser';
@ -10,23 +10,14 @@ import { NAIVE_FIX_PROMPT } from './prompt';
import { logAiEvent } from '../helpers'; import { logAiEvent } from '../helpers';
export class N8nOutputFixingParser extends BaseOutputParser { export class N8nOutputFixingParser extends BaseOutputParser {
private context: IExecuteFunctions;
private model: BaseLanguageModel;
private outputParser: N8nStructuredOutputParser;
lc_namespace = ['langchain', 'output_parsers', 'fix']; lc_namespace = ['langchain', 'output_parsers', 'fix'];
constructor( constructor(
context: IExecuteFunctions, private context: ISupplyDataFunctions,
model: BaseLanguageModel, private model: BaseLanguageModel,
outputParser: N8nStructuredOutputParser, private outputParser: N8nStructuredOutputParser,
) { ) {
super(); super();
this.context = context;
this.model = model;
this.outputParser = outputParser;
} }
getRetryChain() { getRetryChain() {
@ -48,7 +39,7 @@ export class N8nOutputFixingParser extends BaseOutputParser {
try { try {
// First attempt to parse the completion // First attempt to parse the completion
const response = await this.outputParser.parse(completion, callbacks, (e) => e); const response = await this.outputParser.parse(completion, callbacks, (e) => e);
void logAiEvent(this.context, 'ai-output-parsed', { text: completion, response }); logAiEvent(this.context, 'ai-output-parsed', { text: completion, response });
this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [ this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [
[{ json: { action: 'parse', response } }], [{ json: { action: 'parse', response } }],

View file

@ -1,7 +1,7 @@
import type { Callbacks } from '@langchain/core/callbacks/manager'; import type { Callbacks } from '@langchain/core/callbacks/manager';
import { StructuredOutputParser } from 'langchain/output_parsers'; import { StructuredOutputParser } from 'langchain/output_parsers';
import get from 'lodash/get'; import get from 'lodash/get';
import type { IExecuteFunctions } from 'n8n-workflow'; import type { ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { z } from 'zod'; import { z } from 'zod';
@ -14,11 +14,11 @@ const STRUCTURED_OUTPUT_ARRAY_KEY = '__structured__output__array';
export class N8nStructuredOutputParser extends StructuredOutputParser< export class N8nStructuredOutputParser extends StructuredOutputParser<
z.ZodType<object, z.ZodTypeDef, object> z.ZodType<object, z.ZodTypeDef, object>
> { > {
context: IExecuteFunctions; constructor(
private context: ISupplyDataFunctions,
constructor(context: IExecuteFunctions, zodSchema: z.ZodSchema<object>) { zodSchema: z.ZodSchema<object>,
) {
super(zodSchema); super(zodSchema);
this.context = context;
} }
lc_namespace = ['langchain', 'output_parsers', 'structured']; lc_namespace = ['langchain', 'output_parsers', 'structured'];
@ -39,7 +39,7 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
get(parsed, STRUCTURED_OUTPUT_KEY) ?? get(parsed, STRUCTURED_OUTPUT_KEY) ??
parsed) as Record<string, unknown>; parsed) as Record<string, unknown>;
void logAiEvent(this.context, 'ai-output-parsed', { text, response: result }); logAiEvent(this.context, 'ai-output-parsed', { text, response: result });
this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [ this.context.addOutputData(NodeConnectionType.AiOutputParser, index, [
[{ json: { action: 'parse', response: result } }], [{ json: { action: 'parse', response: result } }],
@ -56,7 +56,7 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
}, },
); );
void logAiEvent(this.context, 'ai-output-parsed', { logAiEvent(this.context, 'ai-output-parsed', {
text, text,
response: e.message ?? e, response: e.message ?? e,
}); });
@ -73,7 +73,7 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
static async fromZodJsonSchema( static async fromZodJsonSchema(
zodSchema: z.ZodSchema<object>, zodSchema: z.ZodSchema<object>,
nodeVersion: number, nodeVersion: number,
context: IExecuteFunctions, context: ISupplyDataFunctions,
): Promise<N8nStructuredOutputParser> { ): Promise<N8nStructuredOutputParser> {
let returnSchema: z.ZodType<object, z.ZodTypeDef, object>; let returnSchema: z.ZodType<object, z.ZodTypeDef, object>;
if (nodeVersion === 1) { if (nodeVersion === 1) {

View file

@ -22,9 +22,11 @@
"dist/**/*" "dist/**/*"
], ],
"dependencies": { "dependencies": {
"@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*", "n8n-workflow": "workspace:*",
"n8n-core": "workspace:*", "n8n-core": "workspace:*",
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"typedi": "catalog:",
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -0,0 +1,16 @@
import { Config, Env } from '@n8n/config';
@Config
export class BaseRunnerConfig {
@Env('N8N_RUNNERS_N8N_URI')
n8nUri: string = '127.0.0.1:5679';
@Env('N8N_RUNNERS_GRANT_TOKEN')
grantToken: string = '';
@Env('N8N_RUNNERS_MAX_PAYLOAD')
maxPayloadSize: number = 1024 * 1024 * 1024;
@Env('N8N_RUNNERS_MAX_CONCURRENCY')
maxConcurrency: number = 5;
}

View file

@ -0,0 +1,10 @@
import { Config, Env } from '@n8n/config';
@Config
export class JsRunnerConfig {
@Env('NODE_FUNCTION_ALLOW_BUILTIN')
allowedBuiltInModules: string = '';
@Env('NODE_FUNCTION_ALLOW_EXTERNAL')
allowedExternalModules: string = '';
}

View file

@ -0,0 +1,13 @@
import { Config, Nested } from '@n8n/config';
import { BaseRunnerConfig } from './base-runner-config';
import { JsRunnerConfig } from './js-runner-config';
@Config
export class MainConfig {
@Nested
baseRunnerConfig!: BaseRunnerConfig;
@Nested
jsRunnerConfig!: JsRunnerConfig;
}

View file

@ -4,7 +4,6 @@ import fs from 'node:fs';
import { builtinModules } from 'node:module'; import { builtinModules } from 'node:module';
import { ValidationError } from '@/js-task-runner/errors/validation-error'; import { ValidationError } from '@/js-task-runner/errors/validation-error';
import type { JsTaskRunnerOpts } from '@/js-task-runner/js-task-runner';
import { import {
JsTaskRunner, JsTaskRunner,
type AllCodeTaskData, type AllCodeTaskData,
@ -13,17 +12,27 @@ import {
import type { Task } from '@/task-runner'; import type { Task } from '@/task-runner';
import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data'; import { newAllCodeTaskData, newTaskWithSettings, withPairedItem, wrapIntoJson } from './test-data';
import type { JsRunnerConfig } from '../../config/js-runner-config';
import { MainConfig } from '../../config/main-config';
import { ExecutionError } from '../errors/execution-error'; import { ExecutionError } from '../errors/execution-error';
jest.mock('ws'); jest.mock('ws');
const defaultConfig = new MainConfig();
describe('JsTaskRunner', () => { describe('JsTaskRunner', () => {
const createRunnerWithOpts = (opts: Partial<JsTaskRunnerOpts> = {}) => const createRunnerWithOpts = (opts: Partial<JsRunnerConfig> = {}) =>
new JsTaskRunner({ new JsTaskRunner({
wsUrl: 'ws://localhost', baseRunnerConfig: {
grantToken: 'grantToken', ...defaultConfig.baseRunnerConfig,
maxConcurrency: 1, grantToken: 'grantToken',
...opts, maxConcurrency: 1,
n8nUri: 'localhost',
},
jsRunnerConfig: {
...defaultConfig.jsRunnerConfig,
...opts,
},
}); });
const defaultTaskRunner = createRunnerWithOpts(); const defaultTaskRunner = createRunnerWithOpts();

View file

@ -30,6 +30,7 @@ import { makeSerializable } from './errors/serializable-error';
import type { RequireResolver } from './require-resolver'; import type { RequireResolver } from './require-resolver';
import { createRequireResolver } from './require-resolver'; import { createRequireResolver } from './require-resolver';
import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation'; import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation';
import type { MainConfig } from '../config/main-config';
export interface JSExecSettings { export interface JSExecSettings {
code: string; code: string;
@ -76,23 +77,6 @@ export interface AllCodeTaskData {
additionalData: PartialAdditionalData; additionalData: PartialAdditionalData;
} }
export interface JsTaskRunnerOpts {
wsUrl: string;
grantToken: string;
maxConcurrency: number;
name?: string;
/**
* List of built-in nodejs modules that are allowed to be required in the
* execution sandbox. Asterisk (*) can be used to allow all.
*/
allowedBuiltInModules?: string;
/**
* List of npm modules that are allowed to be required in the execution
* sandbox. Asterisk (*) can be used to allow all.
*/
allowedExternalModules?: string;
}
type CustomConsole = { type CustomConsole = {
log: (...args: unknown[]) => void; log: (...args: unknown[]) => void;
}; };
@ -100,22 +84,20 @@ type CustomConsole = {
export class JsTaskRunner extends TaskRunner { export class JsTaskRunner extends TaskRunner {
private readonly requireResolver: RequireResolver; private readonly requireResolver: RequireResolver;
constructor({ constructor(config: MainConfig, name = 'JS Task Runner') {
grantToken, super({
maxConcurrency, taskType: 'javascript',
wsUrl, name,
name = 'JS Task Runner', ...config.baseRunnerConfig,
allowedBuiltInModules, });
allowedExternalModules, const { jsRunnerConfig } = config;
}: JsTaskRunnerOpts) {
super('javascript', wsUrl, grantToken, maxConcurrency, name);
const parseModuleAllowList = (moduleList: string) => const parseModuleAllowList = (moduleList: string) =>
moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim())); moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim()));
this.requireResolver = createRequireResolver({ this.requireResolver = createRequireResolver({
allowedBuiltInModules: parseModuleAllowList(allowedBuiltInModules ?? ''), allowedBuiltInModules: parseModuleAllowList(jsRunnerConfig.allowedBuiltInModules ?? ''),
allowedExternalModules: parseModuleAllowList(allowedExternalModules ?? ''), allowedExternalModules: parseModuleAllowList(jsRunnerConfig.allowedExternalModules ?? ''),
}); });
} }

View file

@ -1,27 +1,12 @@
import { ApplicationError, ensureError } from 'n8n-workflow'; import { ensureError } from 'n8n-workflow';
import Container from 'typedi';
import { MainConfig } from './config/main-config';
import { JsTaskRunner } from './js-task-runner/js-task-runner'; import { JsTaskRunner } from './js-task-runner/js-task-runner';
let runner: JsTaskRunner | undefined; let runner: JsTaskRunner | undefined;
let isShuttingDown = false; let isShuttingDown = false;
type Config = {
n8nUri: string;
grantToken: string;
};
function readAndParseConfig(): Config {
const grantToken = process.env.N8N_RUNNERS_GRANT_TOKEN;
if (!grantToken) {
throw new ApplicationError('Missing N8N_RUNNERS_GRANT_TOKEN environment variable');
}
return {
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? '127.0.0.1:5679',
grantToken,
};
}
function createSignalHandler(signal: string) { function createSignalHandler(signal: string) {
return async function onSignal() { return async function onSignal() {
if (isShuttingDown) { if (isShuttingDown) {
@ -46,16 +31,9 @@ function createSignalHandler(signal: string) {
} }
void (async function start() { void (async function start() {
const config = readAndParseConfig(); const config = Container.get(MainConfig);
const wsUrl = `ws://${config.n8nUri}/runners/_ws`; runner = new JsTaskRunner(config);
runner = new JsTaskRunner({
wsUrl,
grantToken: config.grantToken,
maxConcurrency: 5,
allowedBuiltInModules: process.env.NODE_FUNCTION_ALLOW_BUILTIN,
allowedExternalModules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL,
});
process.on('SIGINT', createSignalHandler('SIGINT')); process.on('SIGINT', createSignalHandler('SIGINT'));
process.on('SIGTERM', createSignalHandler('SIGTERM')); process.on('SIGTERM', createSignalHandler('SIGTERM'));

View file

@ -1,8 +1,8 @@
import { ApplicationError, type INodeTypeDescription } from 'n8n-workflow'; import { ApplicationError, type INodeTypeDescription } from 'n8n-workflow';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { URL } from 'node:url';
import { type MessageEvent, WebSocket } from 'ws'; import { type MessageEvent, WebSocket } from 'ws';
import type { BaseRunnerConfig } from './config/base-runner-config';
import { TaskRunnerNodeTypes } from './node-types'; import { TaskRunnerNodeTypes } from './node-types';
import { import {
RPC_ALLOW_LIST, RPC_ALLOW_LIST,
@ -42,7 +42,10 @@ export interface RPCCallObject {
const VALID_TIME_MS = 1000; const VALID_TIME_MS = 1000;
const VALID_EXTRA_MS = 100; const VALID_EXTRA_MS = 100;
const DEFAULT_MAX_PAYLOAD_SIZE = 1024 * 1024 * 1024; export interface TaskRunnerOpts extends BaseRunnerConfig {
taskType: string;
name?: string;
}
export abstract class TaskRunner { export abstract class TaskRunner {
id: string = nanoid(); id: string = nanoid();
@ -63,22 +66,23 @@ export abstract class TaskRunner {
nodeTypes: TaskRunnerNodeTypes = new TaskRunnerNodeTypes([]); nodeTypes: TaskRunnerNodeTypes = new TaskRunnerNodeTypes([]);
constructor( taskType: string;
public taskType: string,
wsUrl: string, maxConcurrency: number;
grantToken: string,
private maxConcurrency: number, name: string;
public name?: string,
) { constructor(opts: TaskRunnerOpts) {
const url = new URL(wsUrl); this.taskType = opts.taskType;
url.searchParams.append('id', this.id); this.name = opts.name ?? 'Node.js Task Runner SDK';
this.ws = new WebSocket(url.toString(), { this.maxConcurrency = opts.maxConcurrency;
const wsUrl = `ws://${opts.n8nUri}/runners/_ws?id=${this.id}`;
this.ws = new WebSocket(wsUrl, {
headers: { headers: {
authorization: `Bearer ${grantToken}`, authorization: `Bearer ${opts.grantToken}`,
}, },
maxPayload: process.env.N8N_RUNNERS_MAX_PAYLOAD maxPayload: opts.maxPayloadSize,
? parseInt(process.env.N8N_RUNNERS_MAX_PAYLOAD)
: DEFAULT_MAX_PAYLOAD_SIZE,
}); });
this.ws.addEventListener('message', this.receiveMessage); this.ws.addEventListener('message', this.receiveMessage);
this.ws.addEventListener('close', this.stopTaskOffers); this.ws.addEventListener('close', this.stopTaskOffers);
@ -145,7 +149,7 @@ export abstract class TaskRunner {
case 'broker:inforequest': case 'broker:inforequest':
this.send({ this.send({
type: 'runner:info', type: 'runner:info',
name: this.name ?? 'Node.js Task Runner SDK', name: this.name,
types: [this.taskType], types: [this.taskType],
}); });
break; break;

View file

@ -2,6 +2,8 @@
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"], "extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
"compilerOptions": { "compilerOptions": {
"rootDir": ".", "rootDir": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": "src", "baseUrl": "src",
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]

View file

@ -17,14 +17,16 @@ const MOCK_ACTIVATION_KEY = 'activation-key';
const MOCK_FEATURE_FLAG = 'feat:sharing'; const MOCK_FEATURE_FLAG = 'feat:sharing';
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26'; const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
describe('License', () => { const licenseConfig: GlobalConfig['license'] = {
beforeAll(() => { serverUrl: MOCK_SERVER_URL,
config.set('license.serverUrl', MOCK_SERVER_URL); autoRenewalEnabled: true,
config.set('license.autoRenewEnabled', true); autoRenewOffset: MOCK_RENEW_OFFSET,
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET); activationKey: MOCK_ACTIVATION_KEY,
config.set('license.tenantId', 1); tenantId: 1,
}); cert: '',
};
describe('License', () => {
let license: License; let license: License;
const instanceSettings = mock<InstanceSettings>({ const instanceSettings = mock<InstanceSettings>({
instanceId: MOCK_INSTANCE_ID, instanceId: MOCK_INSTANCE_ID,
@ -32,7 +34,10 @@ describe('License', () => {
}); });
beforeEach(async () => { beforeEach(async () => {
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: false } }); const globalConfig = mock<GlobalConfig>({
license: licenseConfig,
multiMainSetup: { enabled: false },
});
license = new License(mockLogger(), instanceSettings, mock(), mock(), mock(), globalConfig); license = new License(mockLogger(), instanceSettings, mock(), mock(), mock(), globalConfig);
await license.init(); await license.init();
}); });
@ -66,7 +71,7 @@ describe('License', () => {
mock(), mock(),
mock(), mock(),
mock(), mock(),
mock(), mock<GlobalConfig>({ license: licenseConfig }),
); );
await license.init(); await license.init();
expect(LicenseManager).toHaveBeenCalledWith( expect(LicenseManager).toHaveBeenCalledWith(
@ -192,17 +197,23 @@ describe('License', () => {
}); });
describe('License', () => { describe('License', () => {
beforeEach(() => {
config.load(config.default);
});
describe('init', () => { describe('init', () => {
describe('in single-main setup', () => { describe('in single-main setup', () => {
describe('with `license.autoRenewEnabled` enabled', () => { describe('with `license.autoRenewEnabled` enabled', () => {
it('should enable renewal', async () => { it('should enable renewal', async () => {
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: false } }); const globalConfig = mock<GlobalConfig>({
license: licenseConfig,
multiMainSetup: { enabled: false },
});
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); await new License(
mockLogger(),
mock<InstanceSettings>({ instanceType: 'main' }),
mock(),
mock(),
mock(),
globalConfig,
).init();
expect(LicenseManager).toHaveBeenCalledWith( expect(LicenseManager).toHaveBeenCalledWith(
expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }), expect.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
@ -212,9 +223,14 @@ describe('License', () => {
describe('with `license.autoRenewEnabled` disabled', () => { describe('with `license.autoRenewEnabled` disabled', () => {
it('should disable renewal', async () => { it('should disable renewal', async () => {
config.set('license.autoRenewEnabled', false); await new License(
mockLogger(),
await new License(mockLogger(), mock(), mock(), mock(), mock(), mock()).init(); mock<InstanceSettings>({ instanceType: 'main' }),
mock(),
mock(),
mock(),
mock(),
).init();
expect(LicenseManager).toHaveBeenCalledWith( expect(LicenseManager).toHaveBeenCalledWith(
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }), expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
@ -228,9 +244,11 @@ describe('License', () => {
test.each(['unset', 'leader', 'follower'])( test.each(['unset', 'leader', 'follower'])(
'if %s status, should disable removal', 'if %s status, should disable removal',
async (status) => { async (status) => {
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } }); const globalConfig = mock<GlobalConfig>({
license: { ...licenseConfig, autoRenewalEnabled: false },
multiMainSetup: { enabled: true },
});
config.set('multiMainSetup.instanceType', status); config.set('multiMainSetup.instanceType', status);
config.set('license.autoRenewEnabled', false);
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
@ -243,9 +261,11 @@ describe('License', () => {
describe('with `license.autoRenewEnabled` enabled', () => { describe('with `license.autoRenewEnabled` enabled', () => {
test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => { test.each(['unset', 'follower'])('if %s status, should disable removal', async (status) => {
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } }); const globalConfig = mock<GlobalConfig>({
license: { ...licenseConfig, autoRenewalEnabled: false },
multiMainSetup: { enabled: true },
});
config.set('multiMainSetup.instanceType', status); config.set('multiMainSetup.instanceType', status);
config.set('license.autoRenewEnabled', false);
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
@ -255,7 +275,10 @@ describe('License', () => {
}); });
it('if leader status, should enable renewal', async () => { it('if leader status, should enable renewal', async () => {
const globalConfig = mock<GlobalConfig>({ multiMainSetup: { enabled: true } }); const globalConfig = mock<GlobalConfig>({
license: licenseConfig,
multiMainSetup: { enabled: true },
});
config.set('multiMainSetup.instanceType', 'leader'); config.set('multiMainSetup.instanceType', 'leader');
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init(); await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();

View file

@ -1,8 +1,8 @@
import { SecurityConfig } from '@n8n/config';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import { ApplicationError } from 'n8n-workflow'; import { ApplicationError } from 'n8n-workflow';
import { Container } from 'typedi'; import { Container } from 'typedi';
import config from '@/config';
import { RISK_CATEGORIES } from '@/security-audit/constants'; import { RISK_CATEGORIES } from '@/security-audit/constants';
import { SecurityAuditService } from '@/security-audit/security-audit.service'; import { SecurityAuditService } from '@/security-audit/security-audit.service';
import type { Risk } from '@/security-audit/types'; import type { Risk } from '@/security-audit/types';
@ -26,7 +26,7 @@ export class SecurityAudit extends BaseCommand {
}), }),
'days-abandoned-workflow': Flags.integer({ 'days-abandoned-workflow': Flags.integer({
default: config.getEnv('security.audit.daysAbandonedWorkflow'), default: Container.get(SecurityConfig).daysAbandonedWorkflow,
description: 'Days for a workflow to be considered abandoned if not executed', description: 'Days for a workflow to be considered abandoned if not executed',
}), }),
}; };

View file

@ -274,7 +274,7 @@ export abstract class BaseCommand extends Command {
this.license = Container.get(License); this.license = Container.get(License);
await this.license.init(); await this.license.init();
const activationKey = config.getEnv('license.activationKey'); const { activationKey } = this.globalConfig.license;
if (activationKey) { if (activationKey) {
const hasCert = (await this.license.loadCertStr()).length > 0; const hasCert = (await this.license.loadCertStr()).length > 0;

View file

@ -199,7 +199,7 @@ export class Start extends BaseCommand {
await this.initOrchestration(); await this.initOrchestration();
this.logger.debug('Orchestration init complete'); this.logger.debug('Orchestration init complete');
if (!config.getEnv('license.autoRenewEnabled') && this.instanceSettings.isLeader) { if (!this.globalConfig.license.autoRenewalEnabled && this.instanceSettings.isLeader) {
this.logger.warn( this.logger.warn(
'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!', 'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!',
); );

View file

@ -187,29 +187,6 @@ export const schema = {
doc: 'Public URL where the editor is accessible. Also used for emails sent from n8n.', doc: 'Public URL where the editor is accessible. Also used for emails sent from n8n.',
}, },
security: {
restrictFileAccessTo: {
doc: 'If set only files in that directories can be accessed. Multiple directories can be separated by semicolon (";").',
format: String,
default: '',
env: 'N8N_RESTRICT_FILE_ACCESS_TO',
},
blockFileAccessToN8nFiles: {
doc: 'If set to true it will block access to all files in the ".n8n" directory, the static cache dir at ~/.cache/n8n/public, and user defined config files.',
format: Boolean,
default: true,
env: 'N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES',
},
audit: {
daysAbandonedWorkflow: {
doc: 'Days for a workflow to be considered abandoned if not executed',
format: Number,
default: 90,
env: 'N8N_SECURITY_AUDIT_DAYS_ABANDONED_WORKFLOW',
},
},
},
workflowTagsDisabled: { workflowTagsDisabled: {
format: Boolean, format: Boolean,
default: false, default: false,
@ -411,45 +388,6 @@ export const schema = {
env: 'N8N_DEFAULT_LOCALE', env: 'N8N_DEFAULT_LOCALE',
}, },
license: {
serverUrl: {
format: String,
default: 'https://license.n8n.io/v1',
env: 'N8N_LICENSE_SERVER_URL',
doc: 'License server url to retrieve license.',
},
autoRenewEnabled: {
format: Boolean,
default: true,
env: 'N8N_LICENSE_AUTO_RENEW_ENABLED',
doc: 'Whether auto renewal for licenses is enabled.',
},
autoRenewOffset: {
format: Number,
default: 60 * 60 * 72, // 72 hours
env: 'N8N_LICENSE_AUTO_RENEW_OFFSET',
doc: 'How many seconds before expiry a license should get automatically renewed. ',
},
activationKey: {
format: String,
default: '',
env: 'N8N_LICENSE_ACTIVATION_KEY',
doc: 'Activation key to initialize license',
},
tenantId: {
format: Number,
default: 1,
env: 'N8N_LICENSE_TENANT_ID',
doc: 'Tenant id used by the license manager',
},
cert: {
format: String,
default: '',
env: 'N8N_LICENSE_CERT',
doc: 'Ephemeral license certificate',
},
},
hideUsagePage: { hideUsagePage: {
format: Boolean, format: Boolean,
default: false, default: false,

View file

@ -1061,6 +1061,7 @@ describe('TelemetryEventRelay', () => {
describe('Community+ registered', () => { describe('Community+ registered', () => {
it('should track `license-community-plus-registered` event', () => { it('should track `license-community-plus-registered` event', () => {
const event: RelayEventMap['license-community-plus-registered'] = { const event: RelayEventMap['license-community-plus-registered'] = {
userId: 'user123',
email: 'user@example.com', email: 'user@example.com',
licenseKey: 'license123', licenseKey: 'license123',
}; };
@ -1068,6 +1069,7 @@ describe('TelemetryEventRelay', () => {
eventService.emit('license-community-plus-registered', event); eventService.emit('license-community-plus-registered', event);
expect(telemetry.track).toHaveBeenCalledWith('User registered for license community plus', { expect(telemetry.track).toHaveBeenCalledWith('User registered for license community plus', {
user_id: 'user123',
email: 'user@example.com', email: 'user@example.com',
licenseKey: 'license123', licenseKey: 'license123',
}); });

View file

@ -8,7 +8,7 @@ import type {
import type { AuthProviderType } from '@/databases/entities/auth-identity'; import type { AuthProviderType } from '@/databases/entities/auth-identity';
import type { ProjectRole } from '@/databases/entities/project-relation'; import type { ProjectRole } from '@/databases/entities/project-relation';
import type { GlobalRole } from '@/databases/entities/user'; import type { GlobalRole, User } from '@/databases/entities/user';
import type { IWorkflowDb } from '@/interfaces'; import type { IWorkflowDb } from '@/interfaces';
import type { AiEventMap } from './ai.event-map'; import type { AiEventMap } from './ai.event-map';
@ -421,6 +421,7 @@ export type RelayEventMap = {
}; };
'license-community-plus-registered': { 'license-community-plus-registered': {
userId: User['id'];
email: string; email: string;
licenseKey: string; licenseKey: string;
}; };

View file

@ -236,10 +236,12 @@ export class TelemetryEventRelay extends EventRelay {
} }
private licenseCommunityPlusRegistered({ private licenseCommunityPlusRegistered({
userId,
email, email,
licenseKey, licenseKey,
}: RelayEventMap['license-community-plus-registered']) { }: RelayEventMap['license-community-plus-registered']) {
this.telemetry.track('User registered for license community plus', { this.telemetry.track('User registered for license community plus', {
user_id: userId,
email, email,
licenseKey, licenseKey,
}); });
@ -778,7 +780,7 @@ export class TelemetryEventRelay extends EventRelay {
ldap_allowed: authenticationMethod === 'ldap', ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml', saml_enabled: authenticationMethod === 'saml',
license_plan_name: this.license.getPlanName(), license_plan_name: this.license.getPlanName(),
license_tenant_id: config.getEnv('license.tenantId'), license_tenant_id: this.globalConfig.license.tenantId,
binary_data_s3: isS3Available && isS3Selected && isS3Licensed, binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled, multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled,
metrics: { metrics: {

View file

@ -48,8 +48,7 @@ export class License {
*/ */
private renewalEnabled() { private renewalEnabled() {
if (this.instanceSettings.instanceType !== 'main') return false; if (this.instanceSettings.instanceType !== 'main') return false;
const autoRenewEnabled = this.globalConfig.license.autoRenewalEnabled;
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
/** /**
* In multi-main setup, all mains start off with `unset` status and so renewal disabled. * In multi-main setup, all mains start off with `unset` status and so renewal disabled.
@ -75,9 +74,9 @@ export class License {
const { instanceType } = this.instanceSettings; const { instanceType } = this.instanceSettings;
const isMainInstance = instanceType === 'main'; const isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl'); const server = this.globalConfig.license.serverUrl;
const offlineMode = !isMainInstance; const offlineMode = !isMainInstance;
const autoRenewOffset = config.getEnv('license.autoRenewOffset'); const autoRenewOffset = this.globalConfig.license.autoRenewOffset;
const saveCertStr = isMainInstance const saveCertStr = isMainInstance
? async (value: TLicenseBlock) => await this.saveCertStr(value) ? async (value: TLicenseBlock) => await this.saveCertStr(value)
: async () => {}; : async () => {};
@ -96,7 +95,7 @@ export class License {
try { try {
this.manager = new LicenseManager({ this.manager = new LicenseManager({
server, server,
tenantId: config.getEnv('license.tenantId'), tenantId: this.globalConfig.license.tenantId,
productIdentifier: `n8n-${N8N_VERSION}`, productIdentifier: `n8n-${N8N_VERSION}`,
autoRenewEnabled: renewalEnabled, autoRenewEnabled: renewalEnabled,
renewOnInit: renewalEnabled, renewOnInit: renewalEnabled,
@ -122,7 +121,7 @@ export class License {
async loadCertStr(): Promise<TLicenseBlock> { async loadCertStr(): Promise<TLicenseBlock> {
// if we have an ephemeral license, we don't want to load it from the database // if we have an ephemeral license, we don't want to load it from the database
const ephemeralLicense = config.get('license.cert'); const ephemeralLicense = this.globalConfig.license.cert;
if (ephemeralLicense) { if (ephemeralLicense) {
return ephemeralLicense; return ephemeralLicense;
} }
@ -179,7 +178,7 @@ export class License {
async saveCertStr(value: TLicenseBlock): Promise<void> { async saveCertStr(value: TLicenseBlock): Promise<void> {
// if we have an ephemeral license, we don't want to save it to the database // if we have an ephemeral license, we don't want to save it to the database
if (config.get('license.cert')) return; if (this.globalConfig.license.cert) return;
await this.settingsRepository.upsert( await this.settingsRepository.upsert(
{ {
key: SETTINGS_LICENSE_CERT_KEY, key: SETTINGS_LICENSE_CERT_KEY,

View file

@ -94,6 +94,7 @@ describe('LicenseService', () => {
.spyOn(axios, 'post') .spyOn(axios, 'post')
.mockResolvedValueOnce({ data: { title: 'Title', text: 'Text', licenseKey: 'abc-123' } }); .mockResolvedValueOnce({ data: { title: 'Title', text: 'Text', licenseKey: 'abc-123' } });
const data = await licenseService.registerCommunityEdition({ const data = await licenseService.registerCommunityEdition({
userId: '123',
email: 'test@ema.il', email: 'test@ema.il',
instanceId: '123', instanceId: '123',
instanceUrl: 'http://localhost', instanceUrl: 'http://localhost',
@ -102,6 +103,7 @@ describe('LicenseService', () => {
expect(data).toEqual({ title: 'Title', text: 'Text' }); expect(data).toEqual({ title: 'Title', text: 'Text' });
expect(eventService.emit).toHaveBeenCalledWith('license-community-plus-registered', { expect(eventService.emit).toHaveBeenCalledWith('license-community-plus-registered', {
userId: '123',
email: 'test@ema.il', email: 'test@ema.il',
licenseKey: 'abc-123', licenseKey: 'abc-123',
}); });
@ -111,6 +113,7 @@ describe('LicenseService', () => {
jest.spyOn(axios, 'post').mockRejectedValueOnce(new AxiosError('Failed')); jest.spyOn(axios, 'post').mockRejectedValueOnce(new AxiosError('Failed'));
await expect( await expect(
licenseService.registerCommunityEdition({ licenseService.registerCommunityEdition({
userId: '123',
email: 'test@ema.il', email: 'test@ema.il',
instanceId: '123', instanceId: '123',
instanceUrl: 'http://localhost', instanceUrl: 'http://localhost',

View file

@ -4,7 +4,7 @@ import { InstanceSettings } from 'n8n-core';
import { Get, Post, RestController, GlobalScope, Body } from '@/decorators'; import { Get, Post, RestController, GlobalScope, Body } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthenticatedRequest, AuthlessRequest, LicenseRequest } from '@/requests'; import { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
import { LicenseService } from './license.service'; import { LicenseService } from './license.service';
@ -41,11 +41,12 @@ export class LicenseController {
@Post('/enterprise/community-registered') @Post('/enterprise/community-registered')
async registerCommunityEdition( async registerCommunityEdition(
_req: AuthlessRequest, req: AuthenticatedRequest,
_res: Response, _res: Response,
@Body payload: CommunityRegisteredRequestDto, @Body payload: CommunityRegisteredRequestDto,
) { ) {
return await this.licenseService.registerCommunityEdition({ return await this.licenseService.registerCommunityEdition({
userId: req.user.id,
email: payload.email, email: payload.email,
instanceId: this.instanceSettings.instanceId, instanceId: this.instanceSettings.instanceId,
instanceUrl: this.urlService.getInstanceBaseUrl(), instanceUrl: this.urlService.getInstanceBaseUrl(),

View file

@ -61,11 +61,13 @@ export class LicenseService {
} }
async registerCommunityEdition({ async registerCommunityEdition({
userId,
email, email,
instanceId, instanceId,
instanceUrl, instanceUrl,
licenseType, licenseType,
}: { }: {
userId: User['id'];
email: string; email: string;
instanceId: string; instanceId: string;
instanceUrl: string; instanceUrl: string;
@ -83,7 +85,7 @@ export class LicenseService {
licenseType, licenseType,
}, },
); );
this.eventService.emit('license-community-plus-registered', { email, licenseKey }); this.eventService.emit('license-community-plus-registered', { userId, email, licenseKey });
return rest; return rest;
} catch (e: unknown) { } catch (e: unknown) {
if (e instanceof AxiosError) { if (e instanceof AxiosError) {

Some files were not shown because too many files have changed in this diff Show more