mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(core): Start modularizing the community packages feature (#17757)
This commit is contained in:
@@ -8,5 +8,12 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": 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',
|
'workflow-activation',
|
||||||
'ssh-client',
|
'ssh-client',
|
||||||
'cron',
|
'cron',
|
||||||
|
'community-nodes',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type LogScope = (typeof LOG_SCOPES)[number];
|
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[] {
|
function isStringArray(input: unknown): input is string[] {
|
||||||
return Array.isArray(input) && input.every((item) => typeof item === '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
|
@Config
|
||||||
export class NodesConfig {
|
export class NodesConfig {
|
||||||
/** Node types to load. Includes all if unspecified. @example '["n8n-nodes-base.hackerNews"]' */
|
/** 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. */
|
/** Whether to enable Python execution on the Code node. */
|
||||||
@Env('N8N_PYTHON_ENABLED')
|
@Env('N8N_PYTHON_ENABLED')
|
||||||
pythonEnabled: boolean = true;
|
pythonEnabled: boolean = true;
|
||||||
|
|
||||||
@Nested
|
|
||||||
communityPackages: CommunityPackagesConfig;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,14 +138,6 @@ describe('GlobalConfig', () => {
|
|||||||
files: [],
|
files: [],
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
communityPackages: {
|
|
||||||
enabled: true,
|
|
||||||
registry: 'https://registry.npmjs.org',
|
|
||||||
reinstallMissing: false,
|
|
||||||
unverifiedEnabled: true,
|
|
||||||
verifiedEnabled: true,
|
|
||||||
preventLoading: false,
|
|
||||||
},
|
|
||||||
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
||||||
include: [],
|
include: [],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { GlobalConfig } from '@n8n/config';
|
|||||||
import type { User, WorkflowEntity } from '@n8n/db';
|
import type { User, WorkflowEntity } from '@n8n/db';
|
||||||
import { WorkflowRepository, DbConnection } from '@n8n/db';
|
import { WorkflowRepository, DbConnection } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import type { SelectQueryBuilder } from '@n8n/typeorm';
|
import { type SelectQueryBuilder } from '@n8n/typeorm';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { IRun } from 'n8n-workflow';
|
import type { IRun } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ActiveExecutions } from '@/active-executions';
|
import { ActiveExecutions } from '@/active-executions';
|
||||||
|
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||||
@@ -33,6 +34,7 @@ mockInstance(MessageEventBus);
|
|||||||
const posthogClient = mockInstance(PostHogClient);
|
const posthogClient = mockInstance(PostHogClient);
|
||||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||||
const externalHooks = mockInstance(ExternalHooks);
|
const externalHooks = mockInstance(ExternalHooks);
|
||||||
|
mockInstance(CommunityPackagesService);
|
||||||
|
|
||||||
const dbConnection = mockInstance(DbConnection);
|
const dbConnection = mockInstance(DbConnection);
|
||||||
dbConnection.init.mockResolvedValue(undefined);
|
dbConnection.init.mockResolvedValue(undefined);
|
||||||
@@ -69,7 +71,7 @@ test('should start a task runner when task runners are enabled', async () => {
|
|||||||
GlobalConfig,
|
GlobalConfig,
|
||||||
mock<GlobalConfig>({
|
mock<GlobalConfig>({
|
||||||
taskRunners: { enabled: true },
|
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 type { IRun } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ActiveExecutions } from '@/active-executions';
|
import { ActiveExecutions } from '@/active-executions';
|
||||||
|
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||||
import { DeprecationService } from '@/deprecation/deprecation.service';
|
import { DeprecationService } from '@/deprecation/deprecation.service';
|
||||||
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
|
||||||
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay';
|
||||||
@@ -32,6 +33,7 @@ mockInstance(MessageEventBus);
|
|||||||
const posthogClient = mockInstance(PostHogClient);
|
const posthogClient = mockInstance(PostHogClient);
|
||||||
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
const telemetryEventRelay = mockInstance(TelemetryEventRelay);
|
||||||
const externalHooks = mockInstance(ExternalHooks);
|
const externalHooks = mockInstance(ExternalHooks);
|
||||||
|
mockInstance(CommunityPackagesService);
|
||||||
|
|
||||||
const dbConnection = mockInstance(DbConnection);
|
const dbConnection = mockInstance(DbConnection);
|
||||||
dbConnection.init.mockResolvedValue(undefined);
|
dbConnection.init.mockResolvedValue(undefined);
|
||||||
@@ -63,7 +65,7 @@ test('should start a task runner when task runners are enabled', async () => {
|
|||||||
GlobalConfig,
|
GlobalConfig,
|
||||||
mock<GlobalConfig>({
|
mock<GlobalConfig>({
|
||||||
taskRunners: { enabled: true },
|
taskRunners: { enabled: true },
|
||||||
nodes: { communityPackages: { enabled: false } },
|
nodes: {},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { NodeTypes } from '@/node-types';
|
|||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { ShutdownService } from '@/shutdown/shutdown.service';
|
import { ShutdownService } from '@/shutdown/shutdown.service';
|
||||||
import { WorkflowHistoryManager } from '@/workflows/workflow-history.ee/workflow-history-manager.ee';
|
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> {
|
export abstract class BaseCommand<F = never> {
|
||||||
readonly flags: F;
|
readonly flags: F;
|
||||||
@@ -132,9 +133,11 @@ export abstract class BaseCommand<F = never> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { communityPackages } = this.globalConfig.nodes;
|
const communityPackagesConfig = Container.get(CommunityPackagesConfig);
|
||||||
if (communityPackages.enabled && this.needsCommunityPackages) {
|
if (communityPackagesConfig.enabled && this.needsCommunityPackages) {
|
||||||
const { CommunityPackagesService } = await import('@/services/community-packages.service');
|
const { CommunityPackagesService } = await import(
|
||||||
|
'@/community-packages/community-packages.service'
|
||||||
|
);
|
||||||
await Container.get(CommunityPackagesService).init();
|
await Container.get(CommunityPackagesService).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Container } from '@n8n/di';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { CredentialsService } from '@/credentials/credentials.service';
|
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';
|
import { BaseCommand } from './base-command';
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { ExecutionsPruningService } from '@/services/pruning/executions-pruning.
|
|||||||
import { UrlService } from '@/services/url.service';
|
import { UrlService } from '@/services/url.service';
|
||||||
import { WaitTracker } from '@/wait-tracker';
|
import { WaitTracker } from '@/wait-tracker';
|
||||||
import { WorkflowRunner } from '@/workflow-runner';
|
import { WorkflowRunner } from '@/workflow-runner';
|
||||||
|
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
@@ -178,14 +179,14 @@ export class Start extends BaseCommand<z.infer<typeof flagsSchema>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { flags } = this;
|
const { flags } = this;
|
||||||
const { communityPackages } = this.globalConfig.nodes;
|
const communityPackagesConfig = Container.get(CommunityPackagesConfig);
|
||||||
// cli flag overrides the config env variable
|
// cli flag overrides the config env variable
|
||||||
if (flags.reinstallMissingPackages) {
|
if (flags.reinstallMissingPackages) {
|
||||||
if (communityPackages.enabled) {
|
if (communityPackagesConfig.enabled) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'`--reinstallMissingPackages` is deprecated: Please use the env variable `N8N_REINSTALL_MISSING_PACKAGES` instead',
|
'`--reinstallMissingPackages` is deprecated: Please use the env variable `N8N_REINSTALL_MISSING_PACKAGES` instead',
|
||||||
);
|
);
|
||||||
communityPackages.reinstallMissing = true;
|
communityPackagesConfig.reinstallMissing = true;
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'`--reinstallMissingPackages` was passed, but community packages are disabled',
|
'`--reinstallMissingPackages` was passed, but community packages are disabled',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { inProduction } from '@n8n/backend-common';
|
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';
|
import { CommunityNodeTypesService } from '../community-node-types.service';
|
||||||
|
|
||||||
jest.mock('@n8n/backend-common', () => ({
|
jest.mock('@n8n/backend-common', () => ({
|
||||||
@@ -8,13 +8,13 @@ jest.mock('@n8n/backend-common', () => ({
|
|||||||
inProduction: jest.fn().mockReturnValue(false),
|
inProduction: jest.fn().mockReturnValue(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../utils/community-node-types-utils', () => ({
|
jest.mock('../community-node-types-utils', () => ({
|
||||||
getCommunityNodeTypes: jest.fn().mockResolvedValue([]),
|
getCommunityNodeTypes: jest.fn().mockResolvedValue([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('CommunityNodeTypesService', () => {
|
describe('CommunityNodeTypesService', () => {
|
||||||
let service: CommunityNodeTypesService;
|
let service: CommunityNodeTypesService;
|
||||||
let globalConfigMock: any;
|
let configMock: any;
|
||||||
let communityPackagesServiceMock: any;
|
let communityPackagesServiceMock: any;
|
||||||
let loggerMock: any;
|
let loggerMock: any;
|
||||||
|
|
||||||
@@ -24,21 +24,13 @@ describe('CommunityNodeTypesService', () => {
|
|||||||
delete process.env.ENVIRONMENT;
|
delete process.env.ENVIRONMENT;
|
||||||
|
|
||||||
loggerMock = { error: jest.fn() };
|
loggerMock = { error: jest.fn() };
|
||||||
globalConfigMock = {
|
configMock = {
|
||||||
nodes: {
|
enabled: true,
|
||||||
communityPackages: {
|
verifiedEnabled: true,
|
||||||
enabled: true,
|
|
||||||
verifiedEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
communityPackagesServiceMock = {};
|
communityPackagesServiceMock = {};
|
||||||
|
|
||||||
service = new CommunityNodeTypesService(
|
service = new CommunityNodeTypesService(loggerMock, configMock, communityPackagesServiceMock);
|
||||||
loggerMock,
|
|
||||||
globalConfigMock,
|
|
||||||
communityPackagesServiceMock,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchNodeTypes', () => {
|
describe('fetchNodeTypes', () => {
|
||||||
@@ -2,13 +2,13 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
|||||||
import type { InstalledPackages } from '@n8n/db';
|
import type { InstalledPackages } from '@n8n/db';
|
||||||
import { mock } from 'jest-mock-extended';
|
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 { NodeRequest } from '@/requests';
|
||||||
|
|
||||||
import type { EventService } from '../../events/event.service';
|
import type { EventService } from '../../events/event.service';
|
||||||
import type { Push } from '../../push';
|
import type { Push } from '../../push';
|
||||||
import type { CommunityNodeTypesService } from '../../services/community-node-types.service';
|
import type { CommunityNodeTypesService } from '../community-node-types.service';
|
||||||
import type { CommunityPackagesService } from '../../services/community-packages.service';
|
import type { CommunityPackagesService } from '../community-packages.service';
|
||||||
|
|
||||||
describe('CommunityPackagesController', () => {
|
describe('CommunityPackagesController', () => {
|
||||||
const push = mock<Push>();
|
const push = mock<Push>();
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Logger } from '@n8n/backend-common';
|
import type { Logger } from '@n8n/backend-common';
|
||||||
import { randomName, mockInstance } from '@n8n/backend-test-utils';
|
import { randomName, mockInstance } from '@n8n/backend-test-utils';
|
||||||
import type { GlobalConfig } from '@n8n/config';
|
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import {
|
import {
|
||||||
InstalledNodes,
|
InstalledNodes,
|
||||||
@@ -17,6 +16,7 @@ import type { InstanceSettings, PackageDirectoryLoader } from 'n8n-core';
|
|||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
import { CommunityPackagesService } from '@/community-packages/community-packages.service';
|
||||||
import {
|
import {
|
||||||
NODE_PACKAGE_PREFIX,
|
NODE_PACKAGE_PREFIX,
|
||||||
NPM_COMMAND_TOKENS,
|
NPM_COMMAND_TOKENS,
|
||||||
@@ -24,14 +24,15 @@ import {
|
|||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||||
import type { CommunityPackages } from '@/interfaces';
|
|
||||||
import type { License } from '@/license';
|
import type { License } from '@/license';
|
||||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||||
import type { Publisher } from '@/scaling/pubsub/publisher.service';
|
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 { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants';
|
||||||
import { mockPackageName, mockPackagePair } from '@test-integration/utils';
|
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('fs/promises');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
@@ -46,14 +47,10 @@ const execMock = ((...args) => {
|
|||||||
|
|
||||||
describe('CommunityPackagesService', () => {
|
describe('CommunityPackagesService', () => {
|
||||||
const license = mock<License>();
|
const license = mock<License>();
|
||||||
const globalConfig = mock<GlobalConfig>({
|
const config = mock<CommunityPackagesConfig>({
|
||||||
nodes: {
|
reinstallMissing: false,
|
||||||
communityPackages: {
|
registry: 'some.random.host',
|
||||||
reinstallMissing: false,
|
unverifiedEnabled: true,
|
||||||
registry: 'some.random.host',
|
|
||||||
unverifiedEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
||||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||||
@@ -72,7 +69,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
loadNodesAndCredentials,
|
loadNodesAndCredentials,
|
||||||
publisher,
|
publisher,
|
||||||
license,
|
license,
|
||||||
globalConfig,
|
config,
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -384,7 +381,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
const testBlockDownloadDir = instanceSettings.nodesDownloadDir;
|
const testBlockDownloadDir = instanceSettings.nodesDownloadDir;
|
||||||
const testBlockPackageDir = `${testBlockDownloadDir}/node_modules/${PACKAGE_NAME}`;
|
const testBlockPackageDir = `${testBlockDownloadDir}/node_modules/${PACKAGE_NAME}`;
|
||||||
const testBlockTarballName = `${PACKAGE_NAME}-latest.tgz`;
|
const testBlockTarballName = `${PACKAGE_NAME}-latest.tgz`;
|
||||||
const testBlockRegistry = globalConfig.nodes.communityPackages.registry;
|
const testBlockRegistry = config.registry;
|
||||||
const testBlockNpmInstallArgs = [
|
const testBlockNpmInstallArgs = [
|
||||||
'--audit=false',
|
'--audit=false',
|
||||||
'--fund=false',
|
'--fund=false',
|
||||||
@@ -519,8 +516,8 @@ describe('CommunityPackagesService', () => {
|
|||||||
|
|
||||||
describe('installPackage', () => {
|
describe('installPackage', () => {
|
||||||
test('should throw when installation of not vetted packages is forbidden', async () => {
|
test('should throw when installation of not vetted packages is forbidden', async () => {
|
||||||
globalConfig.nodes.communityPackages.unverifiedEnabled = false;
|
config.unverifiedEnabled = false;
|
||||||
globalConfig.nodes.communityPackages.registry = 'https://registry.npmjs.org';
|
config.registry = 'https://registry.npmjs.org';
|
||||||
await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow(
|
await expect(communityPackagesService.installPackage('package', '0.1.0')).rejects.toThrow(
|
||||||
'Installation of unverified community packages is forbidden!',
|
'Installation of unverified community packages is forbidden!',
|
||||||
);
|
);
|
||||||
@@ -601,7 +598,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
loadNodesAndCredentials.isKnownNode.mockImplementation(
|
loadNodesAndCredentials.isKnownNode.mockImplementation(
|
||||||
(nodeType) => nodeType === 'node-type-2',
|
(nodeType) => nodeType === 'node-type-2',
|
||||||
);
|
);
|
||||||
globalConfig.nodes.communityPackages.reinstallMissing = false;
|
config.reinstallMissing = false;
|
||||||
|
|
||||||
await communityPackagesService.checkForMissingPackages();
|
await communityPackagesService.checkForMissingPackages();
|
||||||
|
|
||||||
@@ -616,7 +613,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
|
|
||||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
config.reinstallMissing = true;
|
||||||
|
|
||||||
await communityPackagesService.checkForMissingPackages();
|
await communityPackagesService.checkForMissingPackages();
|
||||||
|
|
||||||
@@ -633,7 +630,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
|
|
||||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
config.reinstallMissing = true;
|
||||||
communityPackagesService.installPackage = jest
|
communityPackagesService.installPackage = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockRejectedValue(new Error('Installation failed'));
|
.mockRejectedValue(new Error('Installation failed'));
|
||||||
@@ -650,7 +647,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
|
|
||||||
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
installedPackageRepository.find.mockResolvedValue(installedPackages);
|
||||||
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
loadNodesAndCredentials.isKnownNode.mockReturnValue(false);
|
||||||
globalConfig.nodes.communityPackages.reinstallMissing = true;
|
config.reinstallMissing = true;
|
||||||
|
|
||||||
// First installation succeeds, second fails
|
// First installation succeeds, second fails
|
||||||
communityPackagesService.installPackage = jest
|
communityPackagesService.installPackage = jest
|
||||||
@@ -2,7 +2,7 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
|||||||
import { Get, RestController } from '@n8n/decorators';
|
import { Get, RestController } from '@n8n/decorators';
|
||||||
import { Request } from 'express';
|
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')
|
@RestController('/community-node-types')
|
||||||
export class CommunityNodeTypesController {
|
export class CommunityNodeTypesController {
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import type { CommunityNodeType } from '@n8n/api-types';
|
import type { CommunityNodeType } from '@n8n/api-types';
|
||||||
import { Logger, inProduction } from '@n8n/backend-common';
|
import { Logger, inProduction } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { ensureError } from 'n8n-workflow';
|
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 { CommunityPackagesService } from './community-packages.service';
|
||||||
import {
|
|
||||||
getCommunityNodeTypes,
|
|
||||||
StrapiCommunityNodeType,
|
|
||||||
} from '../utils/community-node-types-utils';
|
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@@ -20,17 +18,14 @@ export class CommunityNodeTypesService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private globalConfig: GlobalConfig,
|
private config: CommunityPackagesConfig,
|
||||||
private communityPackagesService: CommunityPackagesService,
|
private communityPackagesService: CommunityPackagesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async fetchNodeTypes() {
|
private async fetchNodeTypes() {
|
||||||
try {
|
try {
|
||||||
let data: StrapiCommunityNodeType[] = [];
|
let data: StrapiCommunityNodeType[] = [];
|
||||||
if (
|
if (this.config.enabled && this.config.verifiedEnabled) {
|
||||||
this.globalConfig.nodes.communityPackages.enabled &&
|
|
||||||
this.globalConfig.nodes.communityPackages.verifiedEnabled
|
|
||||||
) {
|
|
||||||
// Cloud sets ENVIRONMENT to 'production' or 'staging' depending on the environment
|
// Cloud sets ENVIRONMENT to 'production' or 'staging' depending on the environment
|
||||||
const environment = this.detectEnvironment();
|
const environment = this.detectEnvironment();
|
||||||
data = await getCommunityNodeTypes(environment);
|
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 {
|
import {
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
STARTER_TEMPLATE_NAME,
|
STARTER_TEMPLATE_NAME,
|
||||||
UNKNOWN_FAILURE_REASON,
|
UNKNOWN_FAILURE_REASON,
|
||||||
} from '@/constants';
|
} 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 { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
import type { CommunityPackages } from '@/interfaces';
|
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { NodeRequest } from '@/requests';
|
import { NodeRequest } from '@/requests';
|
||||||
import { CommunityPackagesService } from '@/services/community-packages.service';
|
|
||||||
|
|
||||||
import { CommunityNodeTypesService } from '../services/community-node-types.service';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PACKAGE_NOT_INSTALLED,
|
PACKAGE_NOT_INSTALLED,
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import type { InstalledPackages } from '@n8n/db';
|
import type { InstalledPackages } from '@n8n/db';
|
||||||
import { InstalledPackagesRepository } from '@n8n/db';
|
import { InstalledPackagesRepository } from '@n8n/db';
|
||||||
@@ -22,13 +21,14 @@ import {
|
|||||||
UNKNOWN_FAILURE_REASON,
|
UNKNOWN_FAILURE_REASON,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
|
||||||
import type { CommunityPackages } from '@/interfaces';
|
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||||
import { toError } from '@/utils';
|
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 DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
||||||
const NPM_COMMON_ARGS = ['--audit=false', '--fund=false'];
|
const NPM_COMMON_ARGS = ['--audit=false', '--fund=false'];
|
||||||
@@ -82,7 +82,7 @@ export class CommunityPackagesService {
|
|||||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
private readonly publisher: Publisher,
|
private readonly publisher: Publisher,
|
||||||
private readonly license: License,
|
private readonly license: License,
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly config: CommunityPackagesConfig,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -312,7 +312,7 @@ export class CommunityPackagesService {
|
|||||||
|
|
||||||
if (missingPackages.size === 0) return;
|
if (missingPackages.size === 0) return;
|
||||||
|
|
||||||
const { reinstallMissing } = this.globalConfig.nodes.communityPackages;
|
const { reinstallMissing } = this.config;
|
||||||
if (reinstallMissing) {
|
if (reinstallMissing) {
|
||||||
this.logger.info('Attempting to reinstall missing packages', { missingPackages });
|
this.logger.info('Attempting to reinstall missing packages', { missingPackages });
|
||||||
try {
|
try {
|
||||||
@@ -365,7 +365,7 @@ export class CommunityPackagesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getNpmRegistry() {
|
private getNpmRegistry() {
|
||||||
const { registry } = this.globalConfig.nodes.communityPackages;
|
const { registry } = this.config;
|
||||||
if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) {
|
if (registry !== DEFAULT_REGISTRY && !this.license.isCustomNpmRegistryEnabled()) {
|
||||||
throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
|
throw new FeatureNotLicensedError(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
|
||||||
}
|
}
|
||||||
@@ -379,7 +379,7 @@ export class CommunityPackagesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private checkInstallPermissions(checksumProvided: boolean) {
|
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!');
|
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;
|
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
|
// telemetry
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import path from 'path';
|
|||||||
import picocolors from 'picocolors';
|
import picocolors from 'picocolors';
|
||||||
|
|
||||||
import { CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, CLI_DIR, inE2ETests } from '@/constants';
|
import { CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, CLI_DIR, inE2ETests } from '@/constants';
|
||||||
|
import { CommunityPackagesConfig } from './community-packages/community-packages.config';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LoadNodesAndCredentials {
|
export class LoadNodesAndCredentials {
|
||||||
@@ -88,7 +89,7 @@ export class LoadNodesAndCredentials {
|
|||||||
await this.loadNodesFromNodeModules(nodeModulesDir, '@n8n/n8n-nodes-langchain');
|
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
|
// Load nodes from any other `n8n-nodes-*` packages in the download directory
|
||||||
// This includes the community nodes
|
// This includes the community nodes
|
||||||
await this.loadNodesFromNodeModules(
|
await this.loadNodesFromNodeModules(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { inDevelopment, Logger } from '@n8n/backend-common';
|
import { inDevelopment, Logger } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { separate } from '@n8n/db';
|
import { separate } from '@n8n/db';
|
||||||
import { Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { IWorkflowBase } from 'n8n-workflow';
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '@/security-audit/constants';
|
} from '@/security-audit/constants';
|
||||||
import type { RiskReporter, Risk, n8n } from '@/security-audit/types';
|
import type { RiskReporter, Risk, n8n } from '@/security-audit/types';
|
||||||
import { toFlaggedNode } from '@/security-audit/utils';
|
import { toFlaggedNode } from '@/security-audit/utils';
|
||||||
|
import { CommunityPackagesConfig } from '@/community-packages/community-packages.config';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class InstanceRiskReporter implements RiskReporter {
|
export class InstanceRiskReporter implements RiskReporter {
|
||||||
@@ -88,7 +89,7 @@ export class InstanceRiskReporter implements RiskReporter {
|
|||||||
const settings: Record<string, unknown> = {};
|
const settings: Record<string, unknown> = {};
|
||||||
|
|
||||||
settings.features = {
|
settings.features = {
|
||||||
communityPackagesEnabled: this.globalConfig.nodes.communityPackages.enabled,
|
communityPackagesEnabled: Container.get(CommunityPackagesConfig).enabled,
|
||||||
versionNotificationsEnabled: this.globalConfig.versionNotifications.enabled,
|
versionNotificationsEnabled: this.globalConfig.versionNotifications.enabled,
|
||||||
templatesEnabled: this.globalConfig.templates.enabled,
|
templatesEnabled: this.globalConfig.templates.enabled,
|
||||||
publicApiEnabled: isApiEnabled(),
|
publicApiEnabled: isApiEnabled(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { GlobalConfig } from '@n8n/config';
|
import { Container, Service } from '@n8n/di';
|
||||||
import { Service } from '@n8n/di';
|
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import type { IWorkflowBase } from 'n8n-workflow';
|
import type { IWorkflowBase } from 'n8n-workflow';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -14,14 +13,14 @@ import {
|
|||||||
} from '@/security-audit/constants';
|
} from '@/security-audit/constants';
|
||||||
import type { Risk, RiskReporter } from '@/security-audit/types';
|
import type { Risk, RiskReporter } from '@/security-audit/types';
|
||||||
import { getNodeTypes } from '@/security-audit/utils';
|
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()
|
@Service()
|
||||||
export class NodesRiskReporter implements RiskReporter {
|
export class NodesRiskReporter implements RiskReporter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
private readonly communityPackagesService: CommunityPackagesService,
|
private readonly communityPackagesService: CommunityPackagesService,
|
||||||
private readonly globalConfig: GlobalConfig,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async report(workflows: IWorkflowBase[]) {
|
async report(workflows: IWorkflowBase[]) {
|
||||||
@@ -87,7 +86,7 @@ export class NodesRiskReporter implements RiskReporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getCommunityNodeDetails() {
|
private async getCommunityNodeDetails() {
|
||||||
if (!this.globalConfig.nodes.communityPackages.enabled) return [];
|
if (!Container.get(CommunityPackagesConfig).enabled) return [];
|
||||||
|
|
||||||
const installedPackages = await this.communityPackagesService.getAllInstalledPackages();
|
const installedPackages = await this.communityPackagesService.getAllInstalledPackages();
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import '@/webhooks/webhooks.controller';
|
|||||||
import { ChatServer } from './chat/chat-server';
|
import { ChatServer } from './chat/chat-server';
|
||||||
|
|
||||||
import { MfaService } from './mfa/mfa.service';
|
import { MfaService } from './mfa/mfa.service';
|
||||||
|
import { CommunityPackagesConfig } from './community-packages/community-packages.config';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class Server extends AbstractServer {
|
export class Server extends AbstractServer {
|
||||||
@@ -118,9 +119,9 @@ export class Server extends AbstractServer {
|
|||||||
await Container.get(LdapService).init();
|
await Container.get(LdapService).init();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.globalConfig.nodes.communityPackages.enabled) {
|
if (Container.get(CommunityPackagesConfig).enabled) {
|
||||||
await import('@/controllers/community-packages.controller');
|
await import('@/community-packages/community-packages.controller');
|
||||||
await import('@/controllers/community-node-types.controller');
|
await import('@/community-packages/community-node-types.controller');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inE2ETests) {
|
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 { 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 { InstanceSettings, BinaryDataConfig } from 'n8n-core';
|
||||||
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
|
||||||
import type { CredentialTypes } from '@/credential-types';
|
import type { CredentialTypes } from '@/credential-types';
|
||||||
import type { CredentialsOverwrites } from '@/credentials-overwrites';
|
import type { CredentialsOverwrites } from '@/credentials-overwrites';
|
||||||
import type { License } from '@/license';
|
import type { License } from '@/license';
|
||||||
import type { UserManagementMailer } from '@/user-management/email';
|
import type { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
||||||
import type { UrlService } from '@/services/url.service';
|
|
||||||
import type { PushConfig } from '@/push/push.config';
|
|
||||||
import type { MfaService } from '@/mfa/mfa.service';
|
import type { MfaService } from '@/mfa/mfa.service';
|
||||||
|
import type { PushConfig } from '@/push/push.config';
|
||||||
import { FrontendService } from '@/services/frontend.service';
|
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', () => {
|
describe('FrontendService', () => {
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
@@ -32,7 +34,7 @@ describe('FrontendService', () => {
|
|||||||
endpoints: { rest: 'rest' },
|
endpoints: { rest: 'rest' },
|
||||||
diagnostics: { enabled: false },
|
diagnostics: { enabled: false },
|
||||||
templates: { enabled: false, host: '' },
|
templates: { enabled: false, host: '' },
|
||||||
nodes: { communityPackages: { enabled: false } },
|
nodes: {},
|
||||||
tags: { disabled: false },
|
tags: { disabled: false },
|
||||||
logging: { level: 'info' },
|
logging: { level: 'info' },
|
||||||
hiringBanner: { enabled: false },
|
hiringBanner: { enabled: false },
|
||||||
@@ -64,6 +66,13 @@ describe('FrontendService', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Container.set(
|
||||||
|
CommunityPackagesConfig,
|
||||||
|
mock<CommunityPackagesConfig>({
|
||||||
|
enabled: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const logger = mock<Logger>();
|
const logger = mock<Logger>();
|
||||||
const instanceSettings = mock<InstanceSettings>({
|
const instanceSettings = mock<InstanceSettings>({
|
||||||
isDocker: false,
|
isDocker: false,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { BinaryDataConfig, InstanceSettings } from 'n8n-core';
|
|||||||
import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow';
|
import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||||
import path from 'path';
|
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 config from '@/config';
|
||||||
import { inE2ETests, N8N_VERSION } from '@/constants';
|
import { inE2ETests, N8N_VERSION } from '@/constants';
|
||||||
import { CredentialTypes } from '@/credential-types';
|
import { CredentialTypes } from '@/credential-types';
|
||||||
@@ -20,7 +22,6 @@ import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
|||||||
import { MfaService } from '@/mfa/mfa.service';
|
import { MfaService } from '@/mfa/mfa.service';
|
||||||
import { isApiEnabled } from '@/public-api';
|
import { isApiEnabled } from '@/public-api';
|
||||||
import { PushConfig } from '@/push/push.config';
|
import { PushConfig } from '@/push/push.config';
|
||||||
import type { CommunityPackagesService } from '@/services/community-packages.service';
|
|
||||||
import { getSamlLoginLabel } from '@/sso.ee/saml/saml-helpers';
|
import { getSamlLoginLabel } from '@/sso.ee/saml/saml-helpers';
|
||||||
import { getCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers';
|
import { getCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers';
|
||||||
import { UserManagementMailer } from '@/user-management/email';
|
import { UserManagementMailer } from '@/user-management/email';
|
||||||
@@ -59,10 +60,12 @@ export class FrontendService {
|
|||||||
|
|
||||||
this.initSettings();
|
this.initSettings();
|
||||||
|
|
||||||
if (this.globalConfig.nodes.communityPackages.enabled) {
|
if (Container.get(CommunityPackagesConfig).enabled) {
|
||||||
void import('@/services/community-packages.service').then(({ CommunityPackagesService }) => {
|
void import('@/community-packages/community-packages.service').then(
|
||||||
this.communityPackagesService = Container.get(CommunityPackagesService);
|
({ CommunityPackagesService }) => {
|
||||||
});
|
this.communityPackagesService = Container.get(CommunityPackagesService);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +200,8 @@ export class FrontendService {
|
|||||||
executionMode: config.getEnv('executions.mode'),
|
executionMode: config.getEnv('executions.mode'),
|
||||||
isMultiMain: this.instanceSettings.isMultiMain,
|
isMultiMain: this.instanceSettings.isMultiMain,
|
||||||
pushBackend: this.pushConfig.backend,
|
pushBackend: this.pushConfig.backend,
|
||||||
communityNodesEnabled: this.globalConfig.nodes.communityPackages.enabled,
|
communityNodesEnabled: Container.get(CommunityPackagesConfig).enabled,
|
||||||
unverifiedCommunityNodesEnabled: this.globalConfig.nodes.communityPackages.unverifiedEnabled,
|
unverifiedCommunityNodesEnabled: Container.get(CommunityPackagesConfig).unverifiedEnabled,
|
||||||
deployment: {
|
deployment: {
|
||||||
type: this.globalConfig.deployment.type,
|
type: this.globalConfig.deployment.type,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Push } from '@/push';
|
|||||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||||
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
||||||
import { ScalingService } from '@/scaling/scaling.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 { TaskBrokerServer } from '@/task-runners/task-broker/task-broker-server';
|
||||||
import { TaskRunnerProcess } from '@/task-runners/task-runner-process';
|
import { TaskRunnerProcess } from '@/task-runners/task-runner-process';
|
||||||
import { Telemetry } from '@/telemetry';
|
import { Telemetry } from '@/telemetry';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { InstalledNodes, InstalledPackages } from '@n8n/db';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
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 { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
|
||||||
import { createOwner } from './shared/db/users';
|
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 { OFFICIAL_RISKY_NODE_TYPES, NODES_REPORT } from '@/security-audit/constants';
|
||||||
import { SecurityAuditService } from '@/security-audit/security-audit.service';
|
import { SecurityAuditService } from '@/security-audit/security-audit.service';
|
||||||
import { toReportTitle } from '@/security-audit/utils';
|
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';
|
import { getRiskSection, MOCK_PACKAGE, saveManualTriggerWorkflow } from './utils';
|
||||||
|
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const setupTestServer = ({
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'community-packages':
|
case 'community-packages':
|
||||||
await import('@/controllers/community-packages.controller');
|
await import('@/community-packages/community-packages.controller');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'me':
|
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 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`.
|
- 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.
|
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
|
- `@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`
|
- `@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
|
- `@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
|
## 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.
|
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`.
|
- **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.
|
- **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