refactor(core): Start modularizing the community packages feature (#17757)

This commit is contained in:
Iván Ovejero
2025-07-31 13:55:38 +02:00
committed by GitHub
parent 1ed8239625
commit 1d31e6a0c4
35 changed files with 171 additions and 172 deletions

View File

@@ -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" }
]
}

View File

@@ -20,6 +20,7 @@ export const LOG_SCOPES = [
'workflow-activation',
'ssh-client',
'cron',
'community-nodes',
] as const;
export type LogScope = (typeof LOG_SCOPES)[number];

View File

@@ -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;
}

View File

@@ -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: [],

View File

@@ -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: {},
}),
);

View File

@@ -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: {},
}),
);

View File

@@ -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();
}

View File

@@ -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';

View File

@@ -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',

View File

@@ -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', () => {

View File

@@ -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>();

View File

@@ -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

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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!');
}
}

View File

@@ -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;
};
}

View File

@@ -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
// ----------------------------------

View File

@@ -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(

View File

@@ -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(),

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,
},

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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':

View File

@@ -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.