mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
refactor(core): Start modularizing the community packages feature (#17757)
This commit is contained in:
@@ -8,5 +8,12 @@
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
{ "path": "../../workflow/tsconfig.build.cjs.json" },
|
||||
{ "path": "../config/tsconfig.build.json" },
|
||||
{ "path": "../constants/tsconfig.build.json" },
|
||||
{ "path": "../decorators/tsconfig.build.json" },
|
||||
{ "path": "../di/tsconfig.build.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const LOG_SCOPES = [
|
||||
'workflow-activation',
|
||||
'ssh-client',
|
||||
'cron',
|
||||
'community-nodes',
|
||||
] as const;
|
||||
|
||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config, Env, Nested } from '../decorators';
|
||||
import { Config, Env } from '../decorators';
|
||||
|
||||
function isStringArray(input: unknown): input is string[] {
|
||||
return Array.isArray(input) && input.every((item) => typeof item === 'string');
|
||||
@@ -20,33 +20,6 @@ class JsonStringArray extends Array<string> {
|
||||
}
|
||||
}
|
||||
|
||||
@Config
|
||||
class CommunityPackagesConfig {
|
||||
/** Whether to enable community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_ENABLED')
|
||||
enabled: boolean = true;
|
||||
|
||||
/** NPM registry URL to pull community packages from */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_REGISTRY')
|
||||
registry: string = 'https://registry.npmjs.org';
|
||||
|
||||
/** Whether to reinstall any missing community packages */
|
||||
@Env('N8N_REINSTALL_MISSING_PACKAGES')
|
||||
reinstallMissing: boolean = false;
|
||||
|
||||
/** Whether to block installation of not verified packages */
|
||||
@Env('N8N_UNVERIFIED_PACKAGES_ENABLED')
|
||||
unverifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to enable and show search suggestion of packages verified by n8n */
|
||||
@Env('N8N_VERIFIED_PACKAGES_ENABLED')
|
||||
verifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to load community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_PREVENT_LOADING')
|
||||
preventLoading: boolean = false;
|
||||
}
|
||||
|
||||
@Config
|
||||
export class NodesConfig {
|
||||
/** Node types to load. Includes all if unspecified. @example '["n8n-nodes-base.hackerNews"]' */
|
||||
@@ -64,7 +37,4 @@ export class NodesConfig {
|
||||
/** Whether to enable Python execution on the Code node. */
|
||||
@Env('N8N_PYTHON_ENABLED')
|
||||
pythonEnabled: boolean = true;
|
||||
|
||||
@Nested
|
||||
communityPackages: CommunityPackagesConfig;
|
||||
}
|
||||
|
||||
@@ -138,14 +138,6 @@ describe('GlobalConfig', () => {
|
||||
files: [],
|
||||
},
|
||||
nodes: {
|
||||
communityPackages: {
|
||||
enabled: true,
|
||||
registry: 'https://registry.npmjs.org',
|
||||
reinstallMissing: false,
|
||||
unverifiedEnabled: true,
|
||||
verifiedEnabled: true,
|
||||
preventLoading: false,
|
||||
},
|
||||
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
||||
include: [],
|
||||
exclude: [],
|
||||
|
||||
@@ -3,11 +3,12 @@ import { GlobalConfig } from '@n8n/config';
|
||||
import type { User, WorkflowEntity } from '@n8n/db';
|
||||
import { WorkflowRepository, DbConnection } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { type SelectQueryBuilder } from '@n8n/typeorm';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IRun } from 'n8n-workflow';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||
@@ -33,6 +34,7 @@ mockInstance(MessageEventBus);
|
||||
const posthogClient = mockInstance(PostHogClient);
|
||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||
const externalHooks = mockInstance(ExternalHooks);
|
||||
mockInstance(CommunityPackagesService);
|
||||
|
||||
const dbConnection = mockInstance(DbConnection);
|
||||
dbConnection.init.mockResolvedValue(undefined);
|
||||
@@ -69,7 +71,7 @@ test('should start a task runner when task runners are enabled', async () => {
|
||||
GlobalConfig,
|
||||
mock<GlobalConfig>({
|
||||
taskRunners: { enabled: true },
|
||||
nodes: { communityPackages: { enabled: false } },
|
||||
nodes: {},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { mock } from 'jest-mock-extended';
|
||||
import type { IRun } from 'n8n-workflow';
|
||||
|
||||
import { ActiveExecutions } from '@/active-executions';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||
@@ -32,6 +33,7 @@ mockInstance(MessageEventBus);
|
||||
const posthogClient = mockInstance(PostHogClient);
|
||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||
const externalHooks = mockInstance(ExternalHooks);
|
||||
mockInstance(CommunityPackagesService);
|
||||
|
||||
const dbConnection = mockInstance(DbConnection);
|
||||
dbConnection.init.mockResolvedValue(undefined);
|
||||
@@ -63,7 +65,7 @@ test('should start a task runner when task runners are enabled', async () => {
|
||||
GlobalConfig,
|
||||
mock<GlobalConfig>({
|
||||
taskRunners: { enabled: true },
|
||||
nodes: { communityPackages: { enabled: false } },
|
||||
nodes: {},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { NodeTypes } from '@/node-types';
|
||||
import { PostHogClient } from '@/posthog';
|
||||
import { ShutdownService } from '@/shutdown/shutdown.service';
|
||||
import { WorkflowHistoryManager } from '@/workflows/workflow-history.ee/workflow-history-manager.ee';
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
|
||||
export abstract class BaseCommand<F = never> {
|
||||
readonly flags: F;
|
||||
@@ -132,9 +133,11 @@ export abstract class BaseCommand<F = never> {
|
||||
);
|
||||
}
|
||||
|
||||
const { communityPackages } = this.globalConfig.nodes;
|
||||
if (communityPackages.enabled && this.needsCommunityPackages) {
|
||||
const { CommunityPackagesService } = await import('@/services/community-packages.service');
|
||||
const communityPackagesConfig = Container.get(CommunityPackagesConfig);
|
||||
if (communityPackagesConfig.enabled && this.needsCommunityPackages) {
|
||||
const { CommunityPackagesService } = await import(
|
||||
'@/community-packages/community-packages.service'
|
||||
);
|
||||
await Container.get(CommunityPackagesService).init();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Container } from '@n8n/di';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CredentialsService } from '@/credentials/credentials.service';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
|
||||
import { BaseCommand } from './base-command';
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { ExecutionsPruningService } from '@/services/pruning/executions-pruning.
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { WaitTracker } from '@/wait-tracker';
|
||||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const open = require('open');
|
||||
@@ -178,14 +179,14 @@ export class Start extends BaseCommand<z.infer<typeof flagsSchema>> {
|
||||
}
|
||||
|
||||
const { flags } = this;
|
||||
const { communityPackages } = this.globalConfig.nodes;
|
||||
const communityPackagesConfig = Container.get(CommunityPackagesConfig);
|
||||
// cli flag overrides the config env variable
|
||||
if (flags.reinstallMissingPackages) {
|
||||
if (communityPackages.enabled) {
|
||||
if (communityPackagesConfig.enabled) {
|
||||
this.logger.warn(
|
||||
'`--reinstallMissingPackages` is deprecated: Please use the env variable `N8N_REINSTALL_MISSING_PACKAGES` instead',
|
||||
);
|
||||
communityPackages.reinstallMissing = true;
|
||||
communityPackagesConfig.reinstallMissing = true;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'`--reinstallMissingPackages` was passed, but community packages are disabled',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { inProduction } from '@n8n/backend-common';
|
||||
|
||||
import { getCommunityNodeTypes } from '../../utils/community-node-types-utils';
|
||||
import { getCommunityNodeTypes } from '../community-node-types-utils';
|
||||
import { CommunityNodeTypesService } from '../community-node-types.service';
|
||||
|
||||
jest.mock('@n8n/backend-common', () => ({
|
||||
@@ -8,13 +8,13 @@ jest.mock('@n8n/backend-common', () => ({
|
||||
inProduction: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/community-node-types-utils', () => ({
|
||||
jest.mock('../community-node-types-utils', () => ({
|
||||
getCommunityNodeTypes: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
describe('CommunityNodeTypesService', () => {
|
||||
let service: CommunityNodeTypesService;
|
||||
let globalConfigMock: any;
|
||||
let configMock: any;
|
||||
let communityPackagesServiceMock: any;
|
||||
let loggerMock: any;
|
||||
|
||||
@@ -24,21 +24,13 @@ describe('CommunityNodeTypesService', () => {
|
||||
delete process.env.ENVIRONMENT;
|
||||
|
||||
loggerMock = { error: jest.fn() };
|
||||
globalConfigMock = {
|
||||
nodes: {
|
||||
communityPackages: {
|
||||
enabled: true,
|
||||
verifiedEnabled: true,
|
||||
},
|
||||
},
|
||||
configMock = {
|
||||
enabled: true,
|
||||
verifiedEnabled: true,
|
||||
};
|
||||
communityPackagesServiceMock = {};
|
||||
|
||||
service = new CommunityNodeTypesService(
|
||||
loggerMock,
|
||||
globalConfigMock,
|
||||
communityPackagesServiceMock,
|
||||
);
|
||||
service = new CommunityNodeTypesService(loggerMock, configMock, communityPackagesServiceMock);
|
||||
});
|
||||
|
||||
describe('fetchNodeTypes', () => {
|
||||
@@ -2,13 +2,13 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
||||
import type { InstalledPackages } from '@n8n/db';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { CommunityPackagesController } from '@/controllers/community-packages.controller';
|
||||
import { CommunityPackagesController } from '@/community-packages/community-packages.controller';
|
||||
import type { NodeRequest } from '@/requests';
|
||||
|
||||
import type { EventService } from '../../events/event.service';
|
||||
import type { Push } from '../../push';
|
||||
import type { CommunityNodeTypesService } from '../../services/community-node-types.service';
|
||||
import type { CommunityPackagesService } from '../../services/community-packages.service';
|
||||
import type { CommunityNodeTypesService } from '../community-node-types.service';
|
||||
import type { CommunityPackagesService } from '../community-packages.service';
|
||||
|
||||
describe('CommunityPackagesController', () => {
|
||||
const push = mock<Push>();
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Logger } from '@n8n/backend-common';
|
||||
import { randomName, mockInstance } from '@n8n/backend-test-utils';
|
||||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import {
|
||||
InstalledNodes,
|
||||
@@ -17,6 +16,7 @@ import type { InstanceSettings, PackageDirectoryLoader } from 'n8n-core';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import {
|
||||
NODE_PACKAGE_PREFIX,
|
||||
NPM_COMMAND_TOKENS,
|
||||
@@ -24,14 +24,15 @@ import {
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
} from '@/constants';
|
||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||
import type { CommunityPackages } from '@/interfaces';
|
||||
import type { License } from '@/license';
|
||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants';
|
||||
import { mockPackageName, mockPackagePair } from '@test-integration/utils';
|
||||
|
||||
import type { CommunityPackagesConfig } from '../community-packages.config';
|
||||
import type { CommunityPackages } from '../community-packages.types';
|
||||
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('child_process');
|
||||
jest.mock('axios');
|
||||
@@ -46,14 +47,10 @@ const execMock = ((...args) => {
|
||||
|
||||
describe('CommunityPackagesService', () => {
|
||||
const license = mock<License>();
|
||||
const globalConfig = mock<GlobalConfig>({
|
||||
nodes: {
|
||||
communityPackages: {
|
||||
reinstallMissing: false,
|
||||
registry: 'some.random.host',
|
||||
unverifiedEnabled: true,
|
||||
},
|
||||
},
|
||||
const config = mock<CommunityPackagesConfig>({
|
||||
reinstallMissing: false,
|
||||
registry: 'some.random.host',
|
||||
unverifiedEnabled: true,
|
||||
});
|
||||
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||
@@ -72,7 +69,7 @@ describe('CommunityPackagesService', () => {
|
||||
loadNodesAndCredentials,
|
||||
publisher,
|
||||
license,
|
||||
globalConfig,
|
||||
config,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -384,7 +381,7 @@ describe('CommunityPackagesService', () => {
|
||||
const testBlockDownloadDir = instanceSettings.nodesDownloadDir;
|
||||
const testBlockPackageDir = `${testBlockDownloadDir}/node_modules/${PACKAGE_NAME}`;
|
||||
const testBlockTarballName = `${PACKAGE_NAME}-latest.tgz`;
|
||||
const testBlockRegistry = globalConfig.nodes.communityPackages.registry;
|
||||
const testBlockRegistry = config.registry;
|
||||
const testBlockNpmInstallArgs = [
|
||||
'--audit=false',
|
||||
'--fund=false',
|
||||
@@ -519,8 +516,8 @@ describe('CommunityPackagesService', () => {
|
||||
|
||||
describe('installPackage', () => {
|
||||
test('should throw when installation of not vetted packages is forbidden', async () => {
|
||||
globalConfig.nodes.communityPackages.unverifiedEnabled = false;
|
||||
globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org';
|
||||
config.unverifiedEnabled = false;
|
||||
config.registry = 'https://registry.npmjs.org';
|
||||
await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow(
|
||||
'Installation of unverified community packages is forbidden!',
|
||||
);
|
||||
@@ -601,7 +598,7 @@ describe('CommunityPackagesService', () => {
|
||||
loadNodesAndCredentials.isKnownNode.mockImplementation(
|
||||
(nodeType) => nodeType === 'node-type-2',
|
||||
);
|
||||
globalConfig.nodes.communityPackages.reinstallMissing = false;
|
||||
config.reinstallMissing = false;
|
||||
|
||||
await communityPackagesService.checkForMissingPackages();
|
||||
|
||||
@@ -616,7 +613,7 @@ describe('CommunityPackagesService', () => {
|
||||
|
||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
||||
config.reinstallMissing = true;
|
||||
|
||||
await communityPackagesService.checkForMissingPackages();
|
||||
|
||||
@@ -633,7 +630,7 @@ describe('CommunityPackagesService', () => {
|
||||
|
||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
||||
config.reinstallMissing = true;
|
||||
communityPackagesService.installPackage = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Installation failed'));
|
||||
@@ -650,7 +647,7 @@ describe('CommunityPackagesService', () => {
|
||||
|
||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
||||
config.reinstallMissing = true;
|
||||
|
||||
// First installation succeeds, second fails
|
||||
communityPackagesService.installPackage = jest
|
||||
@@ -2,7 +2,7 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
||||
import { Get, RestController } from '@n8n/decorators';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { CommunityNodeTypesService } from '@/services/community-node-types.service';
|
||||
import { CommunityNodeTypesService } from '@/community-packages/community-node-types.service';
|
||||
|
||||
@RestController('/community-node-types')
|
||||
export class CommunityNodeTypesController {
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { CommunityNodeType } from '@n8n/api-types';
|
||||
import { Logger, inProduction } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import { ensureError } from 'n8n-workflow';
|
||||
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
|
||||
import { getCommunityNodeTypes, StrapiCommunityNodeType } from './community-node-types-utils';
|
||||
import { CommunityPackagesService } from './community-packages.service';
|
||||
import {
|
||||
getCommunityNodeTypes,
|
||||
StrapiCommunityNodeType,
|
||||
} from '../utils/community-node-types-utils';
|
||||
|
||||
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
||||
|
||||
@@ -20,17 +18,14 @@ export class CommunityNodeTypesService {
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private globalConfig: GlobalConfig,
|
||||
private config: CommunityPackagesConfig,
|
||||
private communityPackagesService: CommunityPackagesService,
|
||||
) {}
|
||||
|
||||
private async fetchNodeTypes() {
|
||||
try {
|
||||
let data: StrapiCommunityNodeType[] = [];
|
||||
if (
|
||||
this.globalConfig.nodes.communityPackages.enabled &&
|
||||
this.globalConfig.nodes.communityPackages.verifiedEnabled
|
||||
) {
|
||||
if (this.config.enabled && this.config.verifiedEnabled) {
|
||||
// Cloud sets ENVIRONMENT to 'production' or 'staging' depending on the environment
|
||||
const environment = this.detectEnvironment();
|
||||
data = await getCommunityNodeTypes(environment);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Config, Env } from '@n8n/config';
|
||||
|
||||
@Config
|
||||
export class CommunityPackagesConfig {
|
||||
/** Whether to enable community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_ENABLED')
|
||||
enabled: boolean = true;
|
||||
|
||||
/** NPM registry URL to pull community packages from */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_REGISTRY')
|
||||
registry: string = 'https://registry.npmjs.org';
|
||||
|
||||
/** Whether to reinstall any missing community packages */
|
||||
@Env('N8N_REINSTALL_MISSING_PACKAGES')
|
||||
reinstallMissing: boolean = false;
|
||||
|
||||
/** Whether to block installation of not verified packages */
|
||||
@Env('N8N_UNVERIFIED_PACKAGES_ENABLED')
|
||||
unverifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to enable and show search suggestion of packages verified by n8n */
|
||||
@Env('N8N_VERIFIED_PACKAGES_ENABLED')
|
||||
verifiedEnabled: boolean = true;
|
||||
|
||||
/** Whether to load community packages */
|
||||
@Env('N8N_COMMUNITY_PACKAGES_PREVENT_LOADING')
|
||||
preventLoading: boolean = false;
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { InstalledPackages } from '@n8n/db';
|
||||
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@n8n/decorators';
|
||||
|
||||
import {
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
STARTER_TEMPLATE_NAME,
|
||||
UNKNOWN_FAILURE_REASON,
|
||||
} from '@/constants';
|
||||
import type { InstalledPackages } from '@n8n/db';
|
||||
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@n8n/decorators';
|
||||
|
||||
import type { CommunityPackages } from './community-packages.types';
|
||||
import { CommunityNodeTypesService } from './community-node-types.service';
|
||||
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { EventService } from '@/events/event.service';
|
||||
import type { CommunityPackages } from '@/interfaces';
|
||||
import { Push } from '@/push';
|
||||
import { NodeRequest } from '@/requests';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
|
||||
import { CommunityNodeTypesService } from '../services/community-node-types.service';
|
||||
|
||||
const {
|
||||
PACKAGE_NOT_INSTALLED,
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||
import type { InstalledPackages } from '@n8n/db';
|
||||
import { InstalledPackagesRepository } from '@n8n/db';
|
||||
@@ -22,13 +21,14 @@ import {
|
||||
UNKNOWN_FAILURE_REASON,
|
||||
} from '@/constants';
|
||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||
import type { CommunityPackages } from '@/interfaces';
|
||||
import { License } from '@/license';
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { toError } from '@/utils';
|
||||
|
||||
import { isVersionExists, verifyIntegrity } from '../utils/npm-utils';
|
||||
import { CommunityPackagesConfig } from './community-packages.config';
|
||||
import type { CommunityPackages } from './community-packages.types';
|
||||
import { isVersionExists, verifyIntegrity } from './npm-utils';
|
||||
|
||||
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
||||
const NPM_COMMON_ARGS = ['--audit=false', '--fund=false'];
|
||||
@@ -82,7 +82,7 @@ export class CommunityPackagesService {
|
||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||
private readonly publisher: Publisher,
|
||||
private readonly license: License,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly config: CommunityPackagesConfig,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
@@ -312,7 +312,7 @@ export class CommunityPackagesService {
|
||||
|
||||
if (missingPackages.size === 0) return;
|
||||
|
||||
const { reinstallMissing } = this.globalConfig.nodes.communityPackages;
|
||||
const { reinstallMissing } = this.config;
|
||||
if (reinstallMissing) {
|
||||
this.logger.info('Attempting to reinstall missing packages', { missingPackages });
|
||||
try {
|
||||
@@ -365,7 +365,7 @@ export class CommunityPackagesService {
|
||||
}
|
||||
|
||||
private getNpmRegistry() {
|
||||
const { registry } = this.globalConfig.nodes.communityPackages;
|
||||
const { registry } = this.config;
|
||||
if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) {
|
||||
throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
|
||||
}
|
||||
@@ -379,7 +379,7 @@ export class CommunityPackagesService {
|
||||
}
|
||||
|
||||
private checkInstallPermissions(checksumProvided: boolean) {
|
||||
if (!this.globalConfig.nodes.communityPackages.unverifiedEnabled && !checksumProvided) {
|
||||
if (!this.config.unverifiedEnabled && !checksumProvided) {
|
||||
throw new UnexpectedError('Installation of unverified community packages is forbidden!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export namespace CommunityPackages {
|
||||
export type ParsedPackageName = {
|
||||
packageName: string;
|
||||
rawString: string;
|
||||
scope?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type AvailableUpdates = {
|
||||
[packageName: string]: {
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
location: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PackageStatusCheck = {
|
||||
status: 'OK' | 'Banned';
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
@@ -153,33 +153,6 @@ export interface IWorkflowStatisticsDataLoaded {
|
||||
dataLoaded: boolean;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// community nodes
|
||||
// ----------------------------------
|
||||
|
||||
export namespace CommunityPackages {
|
||||
export type ParsedPackageName = {
|
||||
packageName: string;
|
||||
rawString: string;
|
||||
scope?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type AvailableUpdates = {
|
||||
[packageName: string]: {
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
location: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PackageStatusCheck = {
|
||||
status: 'OK' | 'Banned';
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// telemetry
|
||||
// ----------------------------------
|
||||
|
||||
@@ -31,6 +31,7 @@ import path from 'path';
|
||||
import picocolors from 'picocolors';
|
||||
|
||||
import { CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, CLI_DIR, inE2ETests } from '@/constants';
|
||||
import { CommunityPackagesConfig } from './community-packages/community-packages.config';
|
||||
|
||||
@Service()
|
||||
export class LoadNodesAndCredentials {
|
||||
@@ -88,7 +89,7 @@ export class LoadNodesAndCredentials {
|
||||
await this.loadNodesFromNodeModules(nodeModulesDir, '@n8n/n8n-nodes-langchain');
|
||||
}
|
||||
|
||||
if (!this.globalConfig.nodes.communityPackages.preventLoading) {
|
||||
if (!Container.get(CommunityPackagesConfig).preventLoading) {
|
||||
// Load nodes from any other `n8n-nodes-*` packages in the download directory
|
||||
// This includes the community nodes
|
||||
await this.loadNodesFromNodeModules(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inDevelopment, Logger } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { separate } from '@n8n/db';
|
||||
import { Service } from '@n8n/di';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import axios from 'axios';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/security-audit/constants';
|
||||
import type { RiskReporter, Risk, n8n } from '@/security-audit/types';
|
||||
import { toFlaggedNode } from '@/security-audit/utils';
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
|
||||
@Service()
|
||||
export class InstanceRiskReporter implements RiskReporter {
|
||||
@@ -88,7 +89,7 @@ export class InstanceRiskReporter implements RiskReporter {
|
||||
const settings: Record<string, unknown> = {};
|
||||
|
||||
settings.features = {
|
||||
communityPackagesEnabled: this.globalConfig.nodes.communityPackages.enabled,
|
||||
communityPackagesEnabled: Container.get(CommunityPackagesConfig).enabled,
|
||||
versionNotificationsEnabled: this.globalConfig.versionNotifications.enabled,
|
||||
templatesEnabled: this.globalConfig.templates.enabled,
|
||||
publicApiEnabled: isApiEnabled(),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import glob from 'fast-glob';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
import * as path from 'path';
|
||||
@@ -14,14 +13,14 @@ import {
|
||||
} from '@/security-audit/constants';
|
||||
import type { Risk, RiskReporter } from '@/security-audit/types';
|
||||
import { getNodeTypes } from '@/security-audit/utils';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
|
||||
@Service()
|
||||
export class NodesRiskReporter implements RiskReporter {
|
||||
constructor(
|
||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||
private readonly communityPackagesService: CommunityPackagesService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
) {}
|
||||
|
||||
async report(workflows: IWorkflowBase[]) {
|
||||
@@ -87,7 +86,7 @@ export class NodesRiskReporter implements RiskReporter {
|
||||
}
|
||||
|
||||
private async getCommunityNodeDetails() {
|
||||
if (!this.globalConfig.nodes.communityPackages.enabled) return [];
|
||||
if (!Container.get(CommunityPackagesConfig).enabled) return [];
|
||||
|
||||
const installedPackages = await this.communityPackagesService.getAllInstalledPackages();
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ import '@/webhooks/webhooks.controller';
|
||||
import { ChatServer } from './chat/chat-server';
|
||||
|
||||
import { MfaService } from './mfa/mfa.service';
|
||||
import { CommunityPackagesConfig } from './community-packages/community-packages.config';
|
||||
|
||||
@Service()
|
||||
export class Server extends AbstractServer {
|
||||
@@ -118,9 +119,9 @@ export class Server extends AbstractServer {
|
||||
await Container.get(LdapService).init();
|
||||
}
|
||||
|
||||
if (this.globalConfig.nodes.communityPackages.enabled) {
|
||||
await import('@/controllers/community-packages.controller');
|
||||
await import('@/controllers/community-node-types.controller');
|
||||
if (Container.get(CommunityPackagesConfig).enabled) {
|
||||
await import('@/community-packages/community-packages.controller');
|
||||
await import('@/community-packages/community-node-types.controller');
|
||||
}
|
||||
|
||||
if (inE2ETests) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { GlobalConfig, SecurityConfig } from '@n8n/config';
|
||||
import type { Logger, LicenseState, ModuleRegistry } from '@n8n/backend-common';
|
||||
import type { GlobalConfig, SecurityConfig } from '@n8n/config';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { InstanceSettings, BinaryDataConfig } from 'n8n-core';
|
||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
|
||||
import type { CredentialTypes } from '@/credential-types';
|
||||
import type { CredentialsOverwrites } from '@/credentials-overwrites';
|
||||
import type { License } from '@/license';
|
||||
import type { UserManagementMailer } from '@/user-management/email';
|
||||
import type { UrlService } from '@/services/url.service';
|
||||
import type { PushConfig } from '@/push/push.config';
|
||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import type { MfaService } from '@/mfa/mfa.service';
|
||||
|
||||
import type { PushConfig } from '@/push/push.config';
|
||||
import { FrontendService } from '@/services/frontend.service';
|
||||
import type { UrlService } from '@/services/url.service';
|
||||
import type { UserManagementMailer } from '@/user-management/email';
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
describe('FrontendService', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
@@ -32,7 +34,7 @@ describe('FrontendService', () => {
|
||||
endpoints: { rest: 'rest' },
|
||||
diagnostics: { enabled: false },
|
||||
templates: { enabled: false, host: '' },
|
||||
nodes: { communityPackages: { enabled: false } },
|
||||
nodes: {},
|
||||
tags: { disabled: false },
|
||||
logging: { level: 'info' },
|
||||
hiringBanner: { enabled: false },
|
||||
@@ -64,6 +66,13 @@ describe('FrontendService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
Container.set(
|
||||
CommunityPackagesConfig,
|
||||
mock<CommunityPackagesConfig>({
|
||||
enabled: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const logger = mock<Logger>();
|
||||
const instanceSettings = mock<InstanceSettings>({
|
||||
isDocker: false,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { BinaryDataConfig, InstanceSettings } from 'n8n-core';
|
||||
import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
import path from 'path';
|
||||
|
||||
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||
import type { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import config from '@/config';
|
||||
import { inE2ETests, N8N_VERSION } from '@/constants';
|
||||
import { CredentialTypes } from '@/credential-types';
|
||||
@@ -20,7 +22,6 @@ import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { MfaService } from '@/mfa/mfa.service';
|
||||
import { isApiEnabled } from '@/public-api';
|
||||
import { PushConfig } from '@/push/push.config';
|
||||
import type { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { getSamlLoginLabel } from '@/sso.ee/saml/saml-helpers';
|
||||
import { getCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers';
|
||||
import { UserManagementMailer } from '@/user-management/email';
|
||||
@@ -59,10 +60,12 @@ export class FrontendService {
|
||||
|
||||
this.initSettings();
|
||||
|
||||
if (this.globalConfig.nodes.communityPackages.enabled) {
|
||||
void import('@/services/community-packages.service').then(({ CommunityPackagesService }) => {
|
||||
this.communityPackagesService = Container.get(CommunityPackagesService);
|
||||
});
|
||||
if (Container.get(CommunityPackagesConfig).enabled) {
|
||||
void import('@/community-packages/community-packages.service').then(
|
||||
({ CommunityPackagesService }) => {
|
||||
this.communityPackagesService = Container.get(CommunityPackagesService);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +200,8 @@ export class FrontendService {
|
||||
executionMode: config.getEnv('executions.mode'),
|
||||
isMultiMain: this.instanceSettings.isMultiMain,
|
||||
pushBackend: this.pushConfig.backend,
|
||||
communityNodesEnabled: this.globalConfig.nodes.communityPackages.enabled,
|
||||
unverifiedCommunityNodesEnabled: this.globalConfig.nodes.communityPackages.unverifiedEnabled,
|
||||
communityNodesEnabled: Container.get(CommunityPackagesConfig).enabled,
|
||||
unverifiedCommunityNodesEnabled: Container.get(CommunityPackagesConfig).unverifiedEnabled,
|
||||
deployment: {
|
||||
type: this.globalConfig.deployment.type,
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Push } from '@/push';
|
||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
||||
import { ScalingService } from '@/scaling/scaling.service';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
import { TaskBrokerServer } from '@/task-runners/task-broker/task-broker-server';
|
||||
import { TaskRunnerProcess } from '@/task-runners/task-runner-process';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { InstalledNodes, InstalledPackages } from '@n8n/db';
|
||||
import path from 'path';
|
||||
|
||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
|
||||
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
|
||||
import { createOwner } from './shared/db/users';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NodeTypes } from '@/node-types';
|
||||
import { OFFICIAL_RISKY_NODE_TYPES, NODES_REPORT } from '@/security-audit/constants';
|
||||
import { SecurityAuditService } from '@/security-audit/security-audit.service';
|
||||
import { toReportTitle } from '@/security-audit/utils';
|
||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||
|
||||
import { getRiskSection, MOCK_PACKAGE, saveManualTriggerWorkflow } from './utils';
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ export const setupTestServer = ({
|
||||
break;
|
||||
|
||||
case 'community-packages':
|
||||
await import('@/controllers/community-packages.controller');
|
||||
await import('@/community-packages/community-packages.controller');
|
||||
break;
|
||||
|
||||
case 'me':
|
||||
|
||||
@@ -49,7 +49,7 @@ Modules are managed via env vars:
|
||||
|
||||
- To enable a module (activate it on instance startup), use the env var `N8N_ENABLED_MODULES`.
|
||||
- To disable a module (skip it on instance startup), use the env var `N8N_DISABLED_MODULES`.
|
||||
- Some modules are **default modules** so they are always enabled unless specifically disabled.
|
||||
- Some modules are **default modules** so they are always enabled unless specifically disabled. To enable a module by default, add it [here](https://github.com/n8n-io/n8n/blob/c0360e52afe9db37d4dd6e00955fa42b0c851904/packages/%40n8n/backend-common/src/modules/module-registry.ts#L26).
|
||||
|
||||
Modules that are under a license flag are automatically skipped on startup if the instance is not licensed to use the feature.
|
||||
|
||||
@@ -225,7 +225,7 @@ Service-level decorators to be aware of:
|
||||
- `@Service()` to make a service usable by the dependency injection container
|
||||
- `@OnLifecycleEvent()` to register a class method to be called on an execution lifecycle event, e.g. `nodeExecuteBefore`, `nodeExecuteAfter`, `workflowExecuteBefore`, and `workflowExecuteAfter`
|
||||
- `@OnPubSubEvent()` to register a class method to be called on receiving a message via Redis pubsub
|
||||
- `@OnLeaderTakeover()` and `@OnLeaderStopdown` to register a class method to be called on leadership transition in a multi-main setup
|
||||
- `@OnLeaderTakeover()` and `@OnLeaderStepdown` to register a class method to be called on leadership transition in a multi-main setup
|
||||
|
||||
## Repositories
|
||||
|
||||
@@ -333,7 +333,7 @@ Currently, testing utilities live partly at `cli` and partly at `@n8n/backend-te
|
||||
|
||||
4. Existing features that are not modules (e.g. LDAP) should be turned into modules over time.
|
||||
|
||||
### FAQs
|
||||
## FAQs
|
||||
|
||||
- **What is a good example of a backend module?** Our first backend module is the `insights` module at `packages/@n8n/modules/insights`.
|
||||
- **My feature is already a separate _package_ at `packages/@n8n/{feature}`. How does this work with modules?** If your feature is already fully decoupled from `cli`, or if you know in advance that your feature will have zero dependencies on `cli`, then you already stand to gain most of the benefits of modularity. In this case, you can add a thin module to `cli` containing an entrypoint to your feature imported from your package, so that your feature is loaded only when needed.
|
||||
Reference in New Issue
Block a user