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",
"esbenp.prettier-vscode",
"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](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

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_N8N_URI",
"N8N_RUNNERS_MAX_PAYLOAD",
"N8N_RUNNERS_MAX_CONCURRENCY",
"NODE_FUNCTION_ALLOW_BUILTIN",
"NODE_FUNCTION_ALLOW_EXTERNAL"
"NODE_FUNCTION_ALLOW_EXTERNAL",
"NODE_OPTIONS"
],
"uid": 2000,
"gid": 2000

View file

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

View file

@ -1,3 +1,4 @@
import type { FrontendBetaFeatures } from '@n8n/config';
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
export interface IVersionNotificationSettings {
@ -169,4 +170,5 @@ export interface FrontendSettings {
security: {
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',
'scaling',
'waiting-executions',
'task-runner',
] as const;
export type LogScope = (typeof LOG_SCOPES)[number];

View file

@ -42,4 +42,12 @@ export class TaskRunnersConfig {
/** Which task runner to launch from the config */
@Env('N8N_RUNNERS_LAUNCHER_RUNNER')
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 { ExternalStorageConfig } from './configs/external-storage.config';
import { GenericConfig } from './configs/generic.config';
import { LicenseConfig } from './configs/license.config';
import { LoggingConfig } from './configs/logging.config';
import { MultiMainSetupConfig } from './configs/multi-main-setup.config';
import { NodesConfig } from './configs/nodes.config';
import { PublicApiConfig } from './configs/public-api.config';
import { TaskRunnersConfig } from './configs/runners.config';
export { TaskRunnersConfig } from './configs/runners.config';
import { ScalingModeConfig } from './configs/scaling-mode.config';
import { SecurityConfig } from './configs/security.config';
import { SentryConfig } from './configs/sentry.config';
import { TemplatesConfig } from './configs/templates.config';
import { UserManagementConfig } from './configs/user-management.config';
import { VersionNotificationsConfig } from './configs/version-notifications.config';
import { WorkflowsConfig } from './configs/workflows.config';
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 type { LogScope } from './configs/logging.config';
@ -102,4 +106,10 @@ export class GlobalConfig {
@Nested
generic: GenericConfig;
@Nested
license: LicenseConfig;
@Nested
security: SecurityConfig;
}

View file

@ -231,6 +231,8 @@ describe('GlobalConfig', () => {
port: 5679,
launcherPath: '',
launcherRunner: 'javascript',
maxOldSpaceSize: '',
maxConcurrency: 5,
},
sentry: {
backendDsn: '',
@ -256,6 +258,19 @@ describe('GlobalConfig', () => {
releaseChannel: 'dev',
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', () => {

View file

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

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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');
const textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const repository = this.getNodeParameter('repository', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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');
const textSplitter = (await this.getInputConnectionData(
NodeConnectionType.AiTextSplitter,

View file

@ -2,9 +2,9 @@
import { BedrockEmbeddings } from '@langchain/aws';
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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');
const credentials = await this.getCredentials<{
apiKey: string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const modelName = this.getNodeParameter('modelName', itemIndex, 'embed-english-v2.0') as string;
const credentials = await this.getCredentials<{ apiKey: string }>('cohereApi');

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const modelName = this.getNodeParameter(
'modelName',

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const model = this.getNodeParameter(
'modelName',

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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 modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
import { OllamaEmbeddings } from '@langchain/ollama';
@ -44,7 +44,7 @@ export class EmbeddingsOllama implements INodeType {
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');
const modelName = this.getNodeParameter('model', itemIndex) as string;
const credentials = await this.getCredentials('ollamaApi');

View file

@ -1,10 +1,10 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type SupplyData,
type ISupplyDataFunctions,
type INodeProperties,
} 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');
const credentials = await this.getCredentials('openAiApi');

View file

@ -3,7 +3,7 @@ import {
NodeConnectionType,
type INodePropertyOptions,
type INodeProperties,
type IExecuteFunctions,
type ISupplyDataFunctions,
type INodeType,
type INodeTypeDescription,
type SupplyData,
@ -20,6 +20,10 @@ const modelField: INodeProperties = {
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Claude 3.5 Sonnet(20241022)',
value: 'claude-3-5-sonnet-20241022',
},
{
name: '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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
type JsonObject,
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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 options = this.getNodeParameter('options', itemIndex, {}) as object;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import { NodeConnectionType } from 'n8n-workflow';
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
ISupplyDataFunctions,
SupplyData,
ILoadOptionsFunctions,
} 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 modelName = this.getNodeParameter('model', itemIndex, '', {

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -2,9 +2,9 @@
import { ChatBedrockConverse } from '@langchain/aws';
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as {

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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<{
apiKey: string;
resourceName: string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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 modelName = this.getNodeParameter('modelName', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
type ILoadOptionsFunctions,
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 privateKey = formatPrivateKey(credentials.privateKey as string);
const email = (credentials.email as string).trim();

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 modelName = this.getNodeParameter('model', itemIndex) as string;

View file

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

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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 workflowId = this.getWorkflow().id;
const memoryInstance = MemoryChatBufferSingleton.getInstance();

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 nodeVersion = this.getNode().typeVersion;

View file

@ -1,5 +1,10 @@
/* 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 { BufferMemory, BufferWindowMemory } from 'langchain/memory';
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 tableName = this.getNodeParameter('tableName', itemIndex, 'n8n_chat_histories') as string;
const sessionId = getSessionId(this, itemIndex);

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeOperationError,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
NodeConnectionType,
} 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 nodeVersion = this.getNode().typeVersion;

View file

@ -1,6 +1,11 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
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 { BufferMemory, BufferWindowMemory } from 'langchain/memory';
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 nodeVersion = this.getNode().typeVersion;

View file

@ -1,7 +1,7 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type ISupplyDataFunctions,
type INodeType,
type INodeTypeDescription,
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<{
apiKey?: string;
apiUrl?: string;

View file

@ -1,6 +1,11 @@
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { NodeConnectionType } from 'n8n-workflow';
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow';
import type {
ISupplyDataFunctions,
INodeType,
INodeTypeDescription,
SupplyData,
} from 'n8n-workflow';
import {
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(
NodeConnectionType.AiLanguageModel,
itemIndex,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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 {
numberOfItems?: number;
separator?: string;

View file

@ -1,9 +1,9 @@
import type { JSONSchema7 } from 'json-schema';
import {
jsonParse,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
NodeOperationError,
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';
// We initialize these even though one of them will always be empty
// it makes it easer to navigate the ternary operator

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
@ -63,7 +63,7 @@ export class RetrieverContextualCompression implements INodeType {
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');
const model = (await this.getInputConnectionData(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} 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');
const options = this.getNodeParameter('options', itemIndex, {}) as { queryCount?: number };

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;

View file

@ -5,7 +5,7 @@ import type {
IExecuteWorkflowInfo,
INodeExecutionData,
IWorkflowBase,
IExecuteFunctions,
ISupplyDataFunctions,
INodeType,
INodeTypeDescription,
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 {
lc_namespace = ['n8n-nodes-langchain', 'retrievers', 'workflow'];
executeFunctions: IExecuteFunctions;
constructor(executeFunctions: IExecuteFunctions, fields: BaseRetrieverInput) {
constructor(
private executeFunctions: ISupplyDataFunctions,
fields: BaseRetrieverInput,
) {
super(fields);
this.executeFunctions = executeFunctions;
}
async _getRelevantDocuments(

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const separator = this.getNodeParameter('separator', itemIndex) as string;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const chunkSize = this.getNodeParameter('chunkSize', itemIndex) as number;

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
import { Calculator } from '@langchain/community/tools/calculator';
@ -43,7 +43,7 @@ export class ToolCalculator implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])],
};
async supplyData(this: IExecuteFunctions): Promise<SupplyData> {
async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
return {
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 { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
ISupplyDataFunctions,
SupplyData,
ExecutionError,
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 workflowMode = this.getMode();

View file

@ -1,8 +1,8 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import type {
IExecuteFunctions,
INodeType,
INodeTypeDescription,
ISupplyDataFunctions,
SupplyData,
IHttpRequestMethods,
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, '_');
try {
tryToParseAlphanumericString(name);

View file

@ -10,7 +10,6 @@ describe('ToolHttpRequest', () => {
const helpers = mock<IExecuteFunctions['helpers']>();
const executeFunctions = mock<IExecuteFunctions>({ helpers });
describe('Binary response', () => {
beforeEach(() => {
jest.resetAllMocks();
executeFunctions.getNode.mockReturnValue(
@ -23,6 +22,7 @@ describe('ToolHttpRequest', () => {
executeFunctions.addInputData.mockReturnValue({ index: 0 });
});
describe('Binary response', () => {
it('should return the error when receiving a binary response', async () => {
helpers.httpRequest.mockResolvedValue({
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 cheerio from 'cheerio';
import * as cheerio from 'cheerio';
import { convert } from 'html-to-text';
import { JSDOM } from 'jsdom';
import get from 'lodash/get';
@ -8,12 +8,12 @@ import unset from 'lodash/unset';
import * as mime from 'mime-types';
import { getOAuth2AdditionalParameters } from 'n8n-nodes-base/dist/nodes/HttpRequest/GenericFunctions';
import type {
IExecuteFunctions,
IDataObject,
IHttpRequestOptions,
IRequestOptionsSimplified,
ExecutionError,
NodeApiError,
ISupplyDataFunctions,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow';
import { z } from 'zod';
@ -28,7 +28,7 @@ import type {
} from './interfaces';
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;
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 additionalOptions = getOAuth2AdditionalParameters(predefinedType);
@ -119,7 +119,7 @@ const predefinedCredentialRequest = async (ctx: IExecuteFunctions, itemIndex: nu
};
export const configureHttpRequestFunction = async (
ctx: IExecuteFunctions,
ctx: ISupplyDataFunctions,
credentialsType: 'predefinedCredentialType' | 'genericCredentialType' | 'none',
itemIndex: number,
) => {
@ -146,7 +146,7 @@ const defaultOptimizer = <T>(response: T) => {
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 onlyContent = ctx.getNodeParameter('onlyContent', itemIndex, false) as boolean;
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) => {
if (typeof response === 'object') {
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 => {
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;
if (optimizeResponse) {
@ -469,7 +469,7 @@ const MODEL_INPUT_DESCRIPTION = {
};
export const updateParametersAndOptions = (options: {
ctx: IExecuteFunctions;
ctx: ISupplyDataFunctions;
itemIndex: number;
toolParameters: ToolParameter[];
placeholdersDefinitions: PlaceholderDefinition[];
@ -558,7 +558,7 @@ export const prepareToolDescription = (
};
export const configureToolFunction = (
ctx: IExecuteFunctions,
ctx: ISupplyDataFunctions,
itemIndex: number,
toolParameters: ToolParameter[],
requestOptions: IHttpRequestOptions,

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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 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 { 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 toolDescription = this.getNodeParameter('description', itemIndex) as string;
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;

View file

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

View file

@ -1,9 +1,9 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
import { WolframAlphaTool } from '@langchain/community/tools/wolframalpha';
@ -49,7 +49,7 @@ export class ToolWolframAlpha implements INodeType {
properties: [getConnectionHintNoticeField([NodeConnectionType.AiAgent])],
};
async supplyData(this: IExecuteFunctions): Promise<SupplyData> {
async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
const credentials = await this.getCredentials('wolframAlphaApi');
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 * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
import type {
IExecuteFunctions,
IExecuteWorkflowInfo,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IWorkflowBase,
ISupplyDataFunctions,
SupplyData,
ExecutionError,
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 description = this.getNodeParameter('description', itemIndex) as string;

View file

@ -1,10 +1,10 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type SupplyData,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
import type { Embeddings } from '@langchain/core/embeddings';
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(
NodeConnectionType.AiEmbedding,
itemIndex,

View file

@ -1,8 +1,8 @@
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const namespace = this.getNodeParameter('pineconeNamespace', itemIndex) as string;

View file

@ -1,7 +1,7 @@
import {
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
NodeConnectionType,
} from 'n8n-workflow';
@ -81,7 +81,7 @@ export class VectorStoreSupabaseLoad implements INodeType {
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');
const tableName = this.getNodeParameter('tableName', itemIndex, '', {

View file

@ -1,8 +1,8 @@
import {
NodeConnectionType,
type IExecuteFunctions,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
} from 'n8n-workflow';
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');
const collectionName = this.getNodeParameter('collectionName', itemIndex) as string;

View file

@ -7,11 +7,8 @@ export class MemoryVectorStoreManager {
private vectorStoreBuffer: Map<string, MemoryVectorStore>;
private embeddings: Embeddings;
private constructor(embeddings: Embeddings) {
private constructor(private embeddings: Embeddings) {
this.vectorStoreBuffer = new Map();
this.embeddings = embeddings;
}
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 { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type {
IExecuteFunctions,
INodeCredentialDescription,
INodeProperties,
INodeExecutionData,
IExecuteFunctions,
INodeTypeDescription,
SupplyData,
ISupplyDataFunctions,
INodeType,
ILoadOptionsFunctions,
INodeListSearchResult,
@ -57,13 +58,13 @@ interface VectorStoreNodeConstructorArgs {
retrieveFields?: INodeProperties[];
updateFields?: INodeProperties[];
populateVectorStore: (
context: IExecuteFunctions,
context: ISupplyDataFunctions,
embeddings: Embeddings,
documents: Array<Document<Record<string, unknown>>>,
itemIndex: number,
) => Promise<void>;
getVectorStoreClient: (
context: IExecuteFunctions,
context: ISupplyDataFunctions,
filter: Record<string, never> | undefined,
embeddings: Embeddings,
itemIndex: number,
@ -281,7 +282,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
});
resultData.push(...serializedDocs);
void logAiEvent(this, 'ai-vector-store-searched', { query: prompt });
logAiEvent(this, 'ai-vector-store-searched', { query: prompt });
}
return [resultData];
@ -311,7 +312,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
try {
await args.populateVectorStore(this, embeddings, processedDocuments, itemIndex);
void logAiEvent(this, 'ai-vector-store-populated');
logAiEvent(this, 'ai-vector-store-populated');
} catch (error) {
throw error;
}
@ -365,7 +366,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
ids: [documentId],
});
void logAiEvent(this, 'ai-vector-store-updated');
logAiEvent(this, 'ai-vector-store-updated');
} catch (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 filter = getMetadataFiltersValues(this, itemIndex);
const embeddings = (await this.getInputConnectionData(

View file

@ -1,6 +1,11 @@
import { pipeline } from 'stream/promises';
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 type { TextSplitter } from '@langchain/textsplitters';
@ -26,25 +31,12 @@ const SUPPORTED_MIME_TYPES = {
};
export class N8nBinaryLoader {
private context: IExecuteFunctions;
private optionsPrefix: string;
private binaryDataKey: string;
private textSplitter?: TextSplitter;
constructor(
context: IExecuteFunctions,
optionsPrefix = '',
binaryDataKey = '',
textSplitter?: TextSplitter,
) {
this.context = context;
this.textSplitter = textSplitter;
this.optionsPrefix = optionsPrefix;
this.binaryDataKey = binaryDataKey;
}
private context: IExecuteFunctions | ISupplyDataFunctions,
private optionsPrefix = '',
private binaryDataKey = '',
private textSplitter?: TextSplitter,
) {}
async processAll(items?: INodeExecutionData[]): Promise<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 { Document } from '@langchain/core/documents';
@ -7,17 +12,11 @@ import { TextLoader } from 'langchain/document_loaders/fs/text';
import { getMetadataFiltersValues } from './helpers';
export class N8nJsonLoader {
private context: IExecuteFunctions;
private optionsPrefix: string;
private textSplitter?: TextSplitter;
constructor(context: IExecuteFunctions, optionsPrefix = '', textSplitter?: TextSplitter) {
this.context = context;
this.textSplitter = textSplitter;
this.optionsPrefix = optionsPrefix;
}
constructor(
private context: IExecuteFunctions | ISupplyDataFunctions,
private optionsPrefix = '',
private textSplitter?: TextSplitter,
) {}
async processAll(items?: INodeExecutionData[]): Promise<Document[]> {
const docs: Document[] = [];

View file

@ -1,6 +1,6 @@
import type { DynamicStructuredToolInput } 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 { StructuredOutputParser } from 'langchain/output_parsers';
import type { ZodTypeAny } from 'zod';
@ -45,12 +45,11 @@ ALL parameters marked as required must be provided`;
};
export class N8nTool extends DynamicStructuredTool {
private context: IExecuteFunctions;
constructor(context: IExecuteFunctions, fields: DynamicStructuredToolInput) {
constructor(
private context: ISupplyDataFunctions,
fields: DynamicStructuredToolInput,
) {
super(fields);
this.context = context;
}
asDynamicTool(): DynamicTool {

View file

@ -5,7 +5,13 @@ import type { BaseMessage } from '@langchain/core/messages';
import type { Tool } from '@langchain/core/tools';
import type { BaseChatMemory } from 'langchain/memory';
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';
@ -20,7 +26,7 @@ function hasMethods<T>(obj: unknown, ...methodNames: Array<string | symbol>): ob
}
export function getMetadataFiltersValues(
ctx: IExecuteFunctions,
ctx: IExecuteFunctions | ISupplyDataFunctions,
itemIndex: number,
): Record<string, never> | undefined {
const options = ctx.getNodeParameter('options', itemIndex, {});
@ -93,7 +99,7 @@ export function getPromptInputByType(options: {
}
export function getSessionId(
ctx: IExecuteFunctions | IWebhookFunctions,
ctx: ISupplyDataFunctions | IWebhookFunctions,
itemIndex: number,
selectorKey = 'sessionIdType',
autoSelect = 'fromInput',
@ -133,13 +139,13 @@ export function getSessionId(
return sessionId;
}
export async function logAiEvent(
executeFunctions: IExecuteFunctions,
export function logAiEvent(
executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
event: AiEvent,
data?: IDataObject,
) {
try {
await executeFunctions.logAiEvent(event, data ? jsonStringify(data) : undefined);
executeFunctions.logAiEvent(event, data ? jsonStringify(data) : undefined);
} catch (error) {
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 { TextSplitter } from '@langchain/textsplitters';
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 { logAiEvent, isToolsInstance, isBaseChatMemory, isBaseChatMessageHistory } from './helpers';
@ -27,7 +27,7 @@ const errorsMap: { [key: string]: { message: string; description: string } } = {
export async function callMethodAsync<T>(
this: T,
parameters: {
executeFunctions: IExecuteFunctions;
executeFunctions: IExecuteFunctions | ISupplyDataFunctions;
connectionType: NodeConnectionType;
currentNodeRunIndex: number;
method: (...args: any[]) => Promise<unknown>;
@ -113,7 +113,7 @@ export function logWrapper(
| VectorStore
| N8nBinaryLoader
| N8nJsonLoader,
executeFunctions: IExecuteFunctions,
executeFunctions: IExecuteFunctions | ISupplyDataFunctions,
) {
return new Proxy(originalInstance, {
get: (target, prop) => {
@ -190,7 +190,7 @@ export function logWrapper(
const payload = { action: 'getMessages', response };
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;
};
} else if (prop === 'addMessage' && 'addMessage' in target) {
@ -207,7 +207,7 @@ export function logWrapper(
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 }]]);
};
}
@ -233,7 +233,7 @@ export function logWrapper(
arguments: [query, config],
})) 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 } }]]);
return response;
};
@ -258,7 +258,7 @@ export function logWrapper(
arguments: [documents],
})) as number[][];
void logAiEvent(executeFunctions, 'ai-document-embedded');
logAiEvent(executeFunctions, 'ai-document-embedded');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response;
};
@ -278,7 +278,7 @@ export function logWrapper(
method: target[prop],
arguments: [query],
})) as number[];
void logAiEvent(executeFunctions, 'ai-query-embedded');
logAiEvent(executeFunctions, 'ai-query-embedded');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response;
};
@ -323,7 +323,7 @@ export function logWrapper(
arguments: [item, itemIndex],
})) as number[];
void logAiEvent(executeFunctions, 'ai-document-processed');
logAiEvent(executeFunctions, 'ai-document-processed');
executeFunctions.addOutputData(connectionType, index, [
[{ json: { response }, pairedItem: { item: itemIndex } }],
]);
@ -349,7 +349,7 @@ export function logWrapper(
arguments: [text],
})) as string[];
void logAiEvent(executeFunctions, 'ai-text-split');
logAiEvent(executeFunctions, 'ai-text-split');
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response;
};
@ -373,7 +373,7 @@ export function logWrapper(
arguments: [query],
})) as string;
void logAiEvent(executeFunctions, 'ai-tool-called', { query, response });
logAiEvent(executeFunctions, 'ai-tool-called', { query, response });
executeFunctions.addOutputData(connectionType, index, [[{ json: { response } }]]);
return response;
};
@ -403,7 +403,7 @@ export function logWrapper(
arguments: [query, k, filter, _callbacks],
})) 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 } }]]);
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 { AIMessage } from '@langchain/core/messages';
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 type { N8nStructuredOutputParser } from './N8nStructuredOutputParser';
@ -10,23 +10,14 @@ import { NAIVE_FIX_PROMPT } from './prompt';
import { logAiEvent } from '../helpers';
export class N8nOutputFixingParser extends BaseOutputParser {
private context: IExecuteFunctions;
private model: BaseLanguageModel;
private outputParser: N8nStructuredOutputParser;
lc_namespace = ['langchain', 'output_parsers', 'fix'];
constructor(
context: IExecuteFunctions,
model: BaseLanguageModel,
outputParser: N8nStructuredOutputParser,
private context: ISupplyDataFunctions,
private model: BaseLanguageModel,
private outputParser: N8nStructuredOutputParser,
) {
super();
this.context = context;
this.model = model;
this.outputParser = outputParser;
}
getRetryChain() {
@ -48,7 +39,7 @@ export class N8nOutputFixingParser extends BaseOutputParser {
try {
// First attempt to parse the completion
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, [
[{ json: { action: 'parse', response } }],

View file

@ -1,7 +1,7 @@
import type { Callbacks } from '@langchain/core/callbacks/manager';
import { StructuredOutputParser } from 'langchain/output_parsers';
import get from 'lodash/get';
import type { IExecuteFunctions } from 'n8n-workflow';
import type { ISupplyDataFunctions } from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { z } from 'zod';
@ -14,11 +14,11 @@ const STRUCTURED_OUTPUT_ARRAY_KEY = '__structured__output__array';
export class N8nStructuredOutputParser extends StructuredOutputParser<
z.ZodType<object, z.ZodTypeDef, object>
> {
context: IExecuteFunctions;
constructor(context: IExecuteFunctions, zodSchema: z.ZodSchema<object>) {
constructor(
private context: ISupplyDataFunctions,
zodSchema: z.ZodSchema<object>,
) {
super(zodSchema);
this.context = context;
}
lc_namespace = ['langchain', 'output_parsers', 'structured'];
@ -39,7 +39,7 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
get(parsed, STRUCTURED_OUTPUT_KEY) ??
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, [
[{ 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,
response: e.message ?? e,
});
@ -73,7 +73,7 @@ export class N8nStructuredOutputParser extends StructuredOutputParser<
static async fromZodJsonSchema(
zodSchema: z.ZodSchema<object>,
nodeVersion: number,
context: IExecuteFunctions,
context: ISupplyDataFunctions,
): Promise<N8nStructuredOutputParser> {
let returnSchema: z.ZodType<object, z.ZodTypeDef, object>;
if (nodeVersion === 1) {

View file

@ -22,9 +22,11 @@
"dist/**/*"
],
"dependencies": {
"@n8n/config": "workspace:*",
"n8n-workflow": "workspace:*",
"n8n-core": "workspace:*",
"nanoid": "^3.3.6",
"typedi": "catalog:",
"ws": "^8.18.0"
},
"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 { ValidationError } from '@/js-task-runner/errors/validation-error';
import type { JsTaskRunnerOpts } from '@/js-task-runner/js-task-runner';
import {
JsTaskRunner,
type AllCodeTaskData,
@ -13,17 +12,27 @@ import {
import type { Task } from '@/task-runner';
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';
jest.mock('ws');
const defaultConfig = new MainConfig();
describe('JsTaskRunner', () => {
const createRunnerWithOpts = (opts: Partial<JsTaskRunnerOpts> = {}) =>
const createRunnerWithOpts = (opts: Partial<JsRunnerConfig> = {}) =>
new JsTaskRunner({
wsUrl: 'ws://localhost',
baseRunnerConfig: {
...defaultConfig.baseRunnerConfig,
grantToken: 'grantToken',
maxConcurrency: 1,
n8nUri: 'localhost',
},
jsRunnerConfig: {
...defaultConfig.jsRunnerConfig,
...opts,
},
});
const defaultTaskRunner = createRunnerWithOpts();

View file

@ -30,6 +30,7 @@ import { makeSerializable } from './errors/serializable-error';
import type { RequireResolver } from './require-resolver';
import { createRequireResolver } from './require-resolver';
import { validateRunForAllItemsOutput, validateRunForEachItemOutput } from './result-validation';
import type { MainConfig } from '../config/main-config';
export interface JSExecSettings {
code: string;
@ -76,23 +77,6 @@ export interface AllCodeTaskData {
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 = {
log: (...args: unknown[]) => void;
};
@ -100,22 +84,20 @@ type CustomConsole = {
export class JsTaskRunner extends TaskRunner {
private readonly requireResolver: RequireResolver;
constructor({
grantToken,
maxConcurrency,
wsUrl,
name = 'JS Task Runner',
allowedBuiltInModules,
allowedExternalModules,
}: JsTaskRunnerOpts) {
super('javascript', wsUrl, grantToken, maxConcurrency, name);
constructor(config: MainConfig, name = 'JS Task Runner') {
super({
taskType: 'javascript',
name,
...config.baseRunnerConfig,
});
const { jsRunnerConfig } = config;
const parseModuleAllowList = (moduleList: string) =>
moduleList === '*' ? null : new Set(moduleList.split(',').map((x) => x.trim()));
this.requireResolver = createRequireResolver({
allowedBuiltInModules: parseModuleAllowList(allowedBuiltInModules ?? ''),
allowedExternalModules: parseModuleAllowList(allowedExternalModules ?? ''),
allowedBuiltInModules: parseModuleAllowList(jsRunnerConfig.allowedBuiltInModules ?? ''),
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';
let runner: JsTaskRunner | undefined;
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) {
return async function onSignal() {
if (isShuttingDown) {
@ -46,16 +31,9 @@ function createSignalHandler(signal: string) {
}
void (async function start() {
const config = readAndParseConfig();
const config = Container.get(MainConfig);
const wsUrl = `ws://${config.n8nUri}/runners/_ws`;
runner = new JsTaskRunner({
wsUrl,
grantToken: config.grantToken,
maxConcurrency: 5,
allowedBuiltInModules: process.env.NODE_FUNCTION_ALLOW_BUILTIN,
allowedExternalModules: process.env.NODE_FUNCTION_ALLOW_EXTERNAL,
});
runner = new JsTaskRunner(config);
process.on('SIGINT', createSignalHandler('SIGINT'));
process.on('SIGTERM', createSignalHandler('SIGTERM'));

View file

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

View file

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

View file

@ -17,14 +17,16 @@ const MOCK_ACTIVATION_KEY = 'activation-key';
const MOCK_FEATURE_FLAG = 'feat:sharing';
const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26';
describe('License', () => {
beforeAll(() => {
config.set('license.serverUrl', MOCK_SERVER_URL);
config.set('license.autoRenewEnabled', true);
config.set('license.autoRenewOffset', MOCK_RENEW_OFFSET);
config.set('license.tenantId', 1);
});
const licenseConfig: GlobalConfig['license'] = {
serverUrl: MOCK_SERVER_URL,
autoRenewalEnabled: true,
autoRenewOffset: MOCK_RENEW_OFFSET,
activationKey: MOCK_ACTIVATION_KEY,
tenantId: 1,
cert: '',
};
describe('License', () => {
let license: License;
const instanceSettings = mock<InstanceSettings>({
instanceId: MOCK_INSTANCE_ID,
@ -32,7 +34,10 @@ describe('License', () => {
});
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);
await license.init();
});
@ -66,7 +71,7 @@ describe('License', () => {
mock(),
mock(),
mock(),
mock(),
mock<GlobalConfig>({ license: licenseConfig }),
);
await license.init();
expect(LicenseManager).toHaveBeenCalledWith(
@ -192,17 +197,23 @@ describe('License', () => {
});
describe('License', () => {
beforeEach(() => {
config.load(config.default);
});
describe('init', () => {
describe('in single-main setup', () => {
describe('with `license.autoRenewEnabled` enabled', () => {
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.objectContaining({ autoRenewEnabled: true, renewOnInit: true }),
@ -212,9 +223,14 @@ describe('License', () => {
describe('with `license.autoRenewEnabled` disabled', () => {
it('should disable renewal', async () => {
config.set('license.autoRenewEnabled', false);
await new License(mockLogger(), mock(), mock(), mock(), mock(), mock()).init();
await new License(
mockLogger(),
mock<InstanceSettings>({ instanceType: 'main' }),
mock(),
mock(),
mock(),
mock(),
).init();
expect(LicenseManager).toHaveBeenCalledWith(
expect.objectContaining({ autoRenewEnabled: false, renewOnInit: false }),
@ -228,9 +244,11 @@ describe('License', () => {
test.each(['unset', 'leader', '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('license.autoRenewEnabled', false);
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
@ -243,9 +261,11 @@ describe('License', () => {
describe('with `license.autoRenewEnabled` enabled', () => {
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('license.autoRenewEnabled', false);
await new License(mockLogger(), mock(), mock(), mock(), mock(), globalConfig).init();
@ -255,7 +275,10 @@ describe('License', () => {
});
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');
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 { ApplicationError } from 'n8n-workflow';
import { Container } from 'typedi';
import config from '@/config';
import { RISK_CATEGORIES } from '@/security-audit/constants';
import { SecurityAuditService } from '@/security-audit/security-audit.service';
import type { Risk } from '@/security-audit/types';
@ -26,7 +26,7 @@ export class SecurityAudit extends BaseCommand {
}),
'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',
}),
};

View file

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

View file

@ -199,7 +199,7 @@ export class Start extends BaseCommand {
await this.initOrchestration();
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(
'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.',
},
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: {
format: Boolean,
default: false,
@ -411,45 +388,6 @@ export const schema = {
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: {
format: Boolean,
default: false,

View file

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

View file

@ -8,7 +8,7 @@ import type {
import type { AuthProviderType } from '@/databases/entities/auth-identity';
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 { AiEventMap } from './ai.event-map';
@ -421,6 +421,7 @@ export type RelayEventMap = {
};
'license-community-plus-registered': {
userId: User['id'];
email: string;
licenseKey: string;
};

View file

@ -236,10 +236,12 @@ export class TelemetryEventRelay extends EventRelay {
}
private licenseCommunityPlusRegistered({
userId,
email,
licenseKey,
}: RelayEventMap['license-community-plus-registered']) {
this.telemetry.track('User registered for license community plus', {
user_id: userId,
email,
licenseKey,
});
@ -778,7 +780,7 @@ export class TelemetryEventRelay extends EventRelay {
ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml',
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,
multi_main_setup_enabled: this.globalConfig.multiMainSetup.enabled,
metrics: {

View file

@ -48,8 +48,7 @@ export class License {
*/
private renewalEnabled() {
if (this.instanceSettings.instanceType !== 'main') return false;
const autoRenewEnabled = config.getEnv('license.autoRenewEnabled');
const autoRenewEnabled = this.globalConfig.license.autoRenewalEnabled;
/**
* 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 isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl');
const server = this.globalConfig.license.serverUrl;
const offlineMode = !isMainInstance;
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
const autoRenewOffset = this.globalConfig.license.autoRenewOffset;
const saveCertStr = isMainInstance
? async (value: TLicenseBlock) => await this.saveCertStr(value)
: async () => {};
@ -96,7 +95,7 @@ export class License {
try {
this.manager = new LicenseManager({
server,
tenantId: config.getEnv('license.tenantId'),
tenantId: this.globalConfig.license.tenantId,
productIdentifier: `n8n-${N8N_VERSION}`,
autoRenewEnabled: renewalEnabled,
renewOnInit: renewalEnabled,
@ -122,7 +121,7 @@ export class License {
async loadCertStr(): Promise<TLicenseBlock> {
// 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) {
return ephemeralLicense;
}
@ -179,7 +178,7 @@ export class License {
async saveCertStr(value: TLicenseBlock): Promise<void> {
// 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(
{
key: SETTINGS_LICENSE_CERT_KEY,

View file

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

View file

@ -4,7 +4,7 @@ import { InstanceSettings } from 'n8n-core';
import { Get, Post, RestController, GlobalScope, Body } from '@/decorators';
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 { LicenseService } from './license.service';
@ -41,11 +41,12 @@ export class LicenseController {
@Post('/enterprise/community-registered')
async registerCommunityEdition(
_req: AuthlessRequest,
req: AuthenticatedRequest,
_res: Response,
@Body payload: CommunityRegisteredRequestDto,
) {
return await this.licenseService.registerCommunityEdition({
userId: req.user.id,
email: payload.email,
instanceId: this.instanceSettings.instanceId,
instanceUrl: this.urlService.getInstanceBaseUrl(),

View file

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

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