diff --git a/packages/@n8n/backend-common/tsconfig.json b/packages/@n8n/backend-common/tsconfig.json index eca44d32aa..1f27af82d2 100644 --- a/packages/@n8n/backend-common/tsconfig.json +++ b/packages/@n8n/backend-common/tsconfig.json @@ -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" } + ] } diff --git a/packages/@n8n/config/src/configs/logging.config.ts b/packages/@n8n/config/src/configs/logging.config.ts index 3b8e272b9c..cb9096429c 100644 --- a/packages/@n8n/config/src/configs/logging.config.ts +++ b/packages/@n8n/config/src/configs/logging.config.ts @@ -20,6 +20,7 @@ export const LOG_SCOPES = [ 'workflow-activation', 'ssh-client', 'cron', + 'community-nodes', ] as const; export type LogScope = (typeof LOG_SCOPES)[number]; diff --git a/packages/@n8n/config/src/configs/nodes.config.ts b/packages/@n8n/config/src/configs/nodes.config.ts index 396ec352b9..6ea8f115f5 100644 --- a/packages/@n8n/config/src/configs/nodes.config.ts +++ b/packages/@n8n/config/src/configs/nodes.config.ts @@ -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 { } } -@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; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 8f2008a711..853f0b8784 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -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: [], diff --git a/packages/cli/src/commands/__tests__/execute-batch.test.ts b/packages/cli/src/commands/__tests__/execute-batch.test.ts index 50da9f6347..498f312914 100644 --- a/packages/cli/src/commands/__tests__/execute-batch.test.ts +++ b/packages/cli/src/commands/__tests__/execute-batch.test.ts @@ -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({ taskRunners: { enabled: true }, - nodes: { communityPackages: { enabled: false } }, + nodes: {}, }), ); diff --git a/packages/cli/src/commands/__tests__/execute.test.ts b/packages/cli/src/commands/__tests__/execute.test.ts index 1934cb8fde..db0ed1165f 100644 --- a/packages/cli/src/commands/__tests__/execute.test.ts +++ b/packages/cli/src/commands/__tests__/execute.test.ts @@ -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({ taskRunners: { enabled: true }, - nodes: { communityPackages: { enabled: false } }, + nodes: {}, }), ); diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index bc0080e0de..54bb986d85 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -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 { readonly flags: F; @@ -132,9 +133,11 @@ export abstract class BaseCommand { ); } - 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(); } diff --git a/packages/cli/src/commands/community-node.ts b/packages/cli/src/commands/community-node.ts index c9b7ff8c8c..98373627e3 100644 --- a/packages/cli/src/commands/community-node.ts +++ b/packages/cli/src/commands/community-node.ts @@ -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'; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 8984a9a89e..2bb409468b 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -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> { } 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', diff --git a/packages/cli/src/services/__tests__/community-node-types.service.test.ts b/packages/cli/src/community-packages/__tests__/community-node-types.service.test.ts similarity index 81% rename from packages/cli/src/services/__tests__/community-node-types.service.test.ts rename to packages/cli/src/community-packages/__tests__/community-node-types.service.test.ts index 5a12ce7c72..8d63c09d17 100644 --- a/packages/cli/src/services/__tests__/community-node-types.service.test.ts +++ b/packages/cli/src/community-packages/__tests__/community-node-types.service.test.ts @@ -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', () => { diff --git a/packages/cli/src/controllers/__tests__/community-packages.controller.test.ts b/packages/cli/src/community-packages/__tests__/community-packages.controller.test.ts similarity index 93% rename from packages/cli/src/controllers/__tests__/community-packages.controller.test.ts rename to packages/cli/src/community-packages/__tests__/community-packages.controller.test.ts index 5f3f491122..e1e44f06d1 100644 --- a/packages/cli/src/controllers/__tests__/community-packages.controller.test.ts +++ b/packages/cli/src/community-packages/__tests__/community-packages.controller.test.ts @@ -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(); diff --git a/packages/cli/src/services/__tests__/community-packages.service.test.ts b/packages/cli/src/community-packages/__tests__/community-packages.service.test.ts similarity index 96% rename from packages/cli/src/services/__tests__/community-packages.service.test.ts rename to packages/cli/src/community-packages/__tests__/community-packages.service.test.ts index cd1d2cda4d..11f92d99c9 100644 --- a/packages/cli/src/services/__tests__/community-packages.service.test.ts +++ b/packages/cli/src/community-packages/__tests__/community-packages.service.test.ts @@ -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(); - const globalConfig = mock({ - nodes: { - communityPackages: { - reinstallMissing: false, - registry: 'some.random.host', - unverifiedEnabled: true, - }, - }, + const config = mock({ + reinstallMissing: false, + registry: 'some.random.host', + unverifiedEnabled: true, }); const loadNodesAndCredentials = mock(); 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 diff --git a/packages/cli/src/utils/__tests__/npm-utils.test.ts b/packages/cli/src/community-packages/__tests__/npm-utils.test.ts similarity index 100% rename from packages/cli/src/utils/__tests__/npm-utils.test.ts rename to packages/cli/src/community-packages/__tests__/npm-utils.test.ts diff --git a/packages/cli/src/utils/__tests__/strapi-utils.test.ts b/packages/cli/src/community-packages/__tests__/strapi-utils.test.ts similarity index 100% rename from packages/cli/src/utils/__tests__/strapi-utils.test.ts rename to packages/cli/src/community-packages/__tests__/strapi-utils.test.ts diff --git a/packages/cli/src/utils/community-node-types-utils.ts b/packages/cli/src/community-packages/community-node-types-utils.ts similarity index 100% rename from packages/cli/src/utils/community-node-types-utils.ts rename to packages/cli/src/community-packages/community-node-types-utils.ts diff --git a/packages/cli/src/controllers/community-node-types.controller.ts b/packages/cli/src/community-packages/community-node-types.controller.ts similarity index 86% rename from packages/cli/src/controllers/community-node-types.controller.ts rename to packages/cli/src/community-packages/community-node-types.controller.ts index d0af778691..e1f48d696f 100644 --- a/packages/cli/src/controllers/community-node-types.controller.ts +++ b/packages/cli/src/community-packages/community-node-types.controller.ts @@ -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 { diff --git a/packages/cli/src/services/community-node-types.service.ts b/packages/cli/src/community-packages/community-node-types.service.ts similarity index 90% rename from packages/cli/src/services/community-node-types.service.ts rename to packages/cli/src/community-packages/community-node-types.service.ts index 901ddadf1e..57e586214a 100644 --- a/packages/cli/src/services/community-node-types.service.ts +++ b/packages/cli/src/community-packages/community-node-types.service.ts @@ -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); diff --git a/packages/cli/src/community-packages/community-packages.config.ts b/packages/cli/src/community-packages/community-packages.config.ts new file mode 100644 index 0000000000..2deb1f2d18 --- /dev/null +++ b/packages/cli/src/community-packages/community-packages.config.ts @@ -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; +} diff --git a/packages/cli/src/controllers/community-packages.controller.ts b/packages/cli/src/community-packages/community-packages.controller.ts similarity index 97% rename from packages/cli/src/controllers/community-packages.controller.ts rename to packages/cli/src/community-packages/community-packages.controller.ts index 01bec06a1f..4a0112e484 100644 --- a/packages/cli/src/controllers/community-packages.controller.ts +++ b/packages/cli/src/community-packages/community-packages.controller.ts @@ -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, diff --git a/packages/cli/src/services/community-packages.service.ts b/packages/cli/src/community-packages/community-packages.service.ts similarity index 97% rename from packages/cli/src/services/community-packages.service.ts rename to packages/cli/src/community-packages/community-packages.service.ts index 24c1a3ba6f..f34726f55b 100644 --- a/packages/cli/src/services/community-packages.service.ts +++ b/packages/cli/src/community-packages/community-packages.service.ts @@ -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!'); } } diff --git a/packages/cli/src/community-packages/community-packages.types.ts b/packages/cli/src/community-packages/community-packages.types.ts new file mode 100644 index 0000000000..213313a51b --- /dev/null +++ b/packages/cli/src/community-packages/community-packages.types.ts @@ -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; + }; +} diff --git a/packages/cli/src/utils/npm-utils.ts b/packages/cli/src/community-packages/npm-utils.ts similarity index 100% rename from packages/cli/src/utils/npm-utils.ts rename to packages/cli/src/community-packages/npm-utils.ts diff --git a/packages/cli/src/utils/strapi-utils.ts b/packages/cli/src/community-packages/strapi-utils.ts similarity index 100% rename from packages/cli/src/utils/strapi-utils.ts rename to packages/cli/src/community-packages/strapi-utils.ts diff --git a/packages/cli/src/interfaces.ts b/packages/cli/src/interfaces.ts index 4fe7c7ef14..d0148a9903 100644 --- a/packages/cli/src/interfaces.ts +++ b/packages/cli/src/interfaces.ts @@ -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 // ---------------------------------- diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index 2985df30b6..e397429f7a 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -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( diff --git a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts index ed556ccd5c..d236f0d16e 100644 --- a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts @@ -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 = {}; 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(), diff --git a/packages/cli/src/security-audit/risk-reporters/nodes-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/nodes-risk-reporter.ts index 64206061f4..1ef9c73938 100644 --- a/packages/cli/src/security-audit/risk-reporters/nodes-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/nodes-risk-reporter.ts @@ -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(); diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 6b8e13f42d..0f9ff9502c 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -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) { diff --git a/packages/cli/src/services/__tests__/frontend.service.test.ts b/packages/cli/src/services/__tests__/frontend.service.test.ts index cf5207c27f..68e6a96518 100644 --- a/packages/cli/src/services/__tests__/frontend.service.test.ts +++ b/packages/cli/src/services/__tests__/frontend.service.test.ts @@ -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({ + enabled: false, + }), + ); + const logger = mock(); const instanceSettings = mock({ isDocker: false, diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index fd2223b10a..6209b97165 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -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, }, diff --git a/packages/cli/test/integration/commands/worker.cmd.test.ts b/packages/cli/test/integration/commands/worker.cmd.test.ts index f6dab5b59c..520a1fee3f 100644 --- a/packages/cli/test/integration/commands/worker.cmd.test.ts +++ b/packages/cli/test/integration/commands/worker.cmd.test.ts @@ -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'; diff --git a/packages/cli/test/integration/community-packages.api.test.ts b/packages/cli/test/integration/community-packages.api.test.ts index d6c75610d3..ca929e51e0 100644 --- a/packages/cli/test/integration/community-packages.api.test.ts +++ b/packages/cli/test/integration/community-packages.api.test.ts @@ -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'; diff --git a/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts b/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts index 8fd6cb41eb..4b9f634850 100644 --- a/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts +++ b/packages/cli/test/integration/security-audit/nodes-risk-reporter.test.ts @@ -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'; diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index f95091a722..c188119a1e 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -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': diff --git a/scripts/backend-module/backend-module.guide.md b/scripts/backend-module/backend-module-guide.md similarity index 97% rename from scripts/backend-module/backend-module.guide.md rename to scripts/backend-module/backend-module-guide.md index 457bd02754..eea28b35fc 100644 --- a/scripts/backend-module/backend-module.guide.md +++ b/scripts/backend-module/backend-module-guide.md @@ -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.