refactor(core): Refactor nodes loading (no-changelog) (#7283)

fixes PAY-605
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-10-09 16:09:23 +02:00 committed by GitHub
parent 789e1e7ed4
commit c5ee06cc61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 603 additions and 683 deletions

View file

@ -11,4 +11,3 @@ packages/**/.turbo
.github .github
*.tsbuildinfo *.tsbuildinfo
packages/cli/dist/**/e2e.* packages/cli/dist/**/e2e.*
packages/cli/dist/ReloadNodesAndCredentials.*

View file

@ -61,8 +61,7 @@
"templates", "templates",
"dist", "dist",
"oclif.manifest.json", "oclif.manifest.json",
"!dist/**/e2e.*", "!dist/**/e2e.*"
"!dist/ReloadNodesAndCredentials.*"
], ],
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-cli": "4.0.0", "@apidevtools/swagger-cli": "4.0.0",

View file

@ -1,17 +1,16 @@
import { Service } from 'typedi';
import { loadClassInIsolation } from 'n8n-core'; import { loadClassInIsolation } from 'n8n-core';
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow'; import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
import { Service } from 'typedi'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
@Service() @Service()
export class CredentialTypes implements ICredentialTypes { export class CredentialTypes implements ICredentialTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) { constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) {}
nodesAndCredentials.credentialTypes = this;
}
recognizes(type: string) { recognizes(type: string) {
return type in this.knownCredentials || type in this.loadedCredentials; const { loadedCredentials, knownCredentials } = this.loadNodesAndCredentials;
return type in knownCredentials || type in loadedCredentials;
} }
getByName(credentialType: string): ICredentialType { getByName(credentialType: string): ICredentialType {
@ -19,14 +18,14 @@ export class CredentialTypes implements ICredentialTypes {
} }
getNodeTypesToTestWith(type: string): string[] { getNodeTypesToTestWith(type: string): string[] {
return this.knownCredentials[type]?.nodesToTestWith ?? []; return this.loadNodesAndCredentials.knownCredentials[type]?.nodesToTestWith ?? [];
} }
/** /**
* Returns all parent types of the given credential type * Returns all parent types of the given credential type
*/ */
getParentTypes(typeName: string): string[] { getParentTypes(typeName: string): string[] {
const extendsArr = this.knownCredentials[typeName]?.extends ?? []; const extendsArr = this.loadNodesAndCredentials.knownCredentials[typeName]?.extends ?? [];
if (extendsArr.length) { if (extendsArr.length) {
extendsArr.forEach((type) => { extendsArr.forEach((type) => {
extendsArr.push(...this.getParentTypes(type)); extendsArr.push(...this.getParentTypes(type));
@ -36,12 +35,11 @@ export class CredentialTypes implements ICredentialTypes {
} }
private getCredential(type: string): LoadedClass<ICredentialType> { private getCredential(type: string): LoadedClass<ICredentialType> {
const loadedCredentials = this.loadedCredentials; const { loadedCredentials, knownCredentials } = this.loadNodesAndCredentials;
if (type in loadedCredentials) { if (type in loadedCredentials) {
return loadedCredentials[type]; return loadedCredentials[type];
} }
const knownCredentials = this.knownCredentials;
if (type in knownCredentials) { if (type in knownCredentials) {
const { className, sourcePath } = knownCredentials[type]; const { className, sourcePath } = knownCredentials[type];
const loaded: ICredentialType = loadClassInIsolation(sourcePath, className); const loaded: ICredentialType = loadClassInIsolation(sourcePath, className);
@ -50,12 +48,4 @@ export class CredentialTypes implements ICredentialTypes {
} }
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`); throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`);
} }
private get loadedCredentials() {
return this.nodesAndCredentials.loaded.credentials;
}
private get knownCredentials() {
return this.nodesAndCredentials.known.credentials;
}
} }

View file

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
@ -89,13 +88,11 @@ const mockNodeTypes: INodeTypes = {
}; };
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
constructor( private credentialTypes = Container.get(CredentialTypes);
encryptionKey: string,
private credentialTypes = Container.get(CredentialTypes), private nodeTypes = Container.get(NodeTypes);
private nodeTypes = Container.get(NodeTypes),
) { private credentialsOverwrites = Container.get(CredentialsOverwrites);
super(encryptionKey);
}
/** /**
* Add the required authentication information to the request * Add the required authentication information to the request
@ -388,7 +385,10 @@ export class CredentialsHelper extends ICredentialsHelper {
const credentialsProperties = this.getCredentialsProperties(type); const credentialsProperties = this.getCredentialsProperties(type);
// Load and apply the credentials overwrites if any exist // Load and apply the credentials overwrites if any exist
const dataWithOverwrites = CredentialsOverwrites().applyOverwrite(type, decryptedDataOriginal); const dataWithOverwrites = this.credentialsOverwrites.applyOverwrite(
type,
decryptedDataOriginal,
);
// Add the default credential values // Add the default credential values
let decryptedData = NodeHelpers.getNodeParameters( let decryptedData = NodeHelpers.getNodeParameters(

View file

@ -1,14 +1,17 @@
import config from '@/config'; import { Service } from 'typedi';
import type { ICredentialDataDecryptedObject } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
import { deepCopy, LoggerProxy as Logger, jsonParse, ICredentialTypes } from 'n8n-workflow'; import { deepCopy, LoggerProxy as Logger, jsonParse } from 'n8n-workflow';
import config from '@/config';
import type { ICredentialsOverwrite } from '@/Interfaces'; import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialTypes } from '@/CredentialTypes';
class CredentialsOverwritesClass { @Service()
export class CredentialsOverwrites {
private overwriteData: ICredentialsOverwrite = {}; private overwriteData: ICredentialsOverwrite = {};
private resolvedTypes: string[] = []; private resolvedTypes: string[] = [];
constructor(private credentialTypes: ICredentialTypes) { constructor(private credentialTypes: CredentialTypes) {
const data = config.getEnv('credentials.overwrite.data'); const data = config.getEnv('credentials.overwrite.data');
const overwriteData = jsonParse<ICredentialsOverwrite>(data, { const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
errorMessage: 'The credentials-overwrite is not valid JSON.', errorMessage: 'The credentials-overwrite is not valid JSON.',
@ -96,20 +99,3 @@ class CredentialsOverwritesClass {
return this.overwriteData; return this.overwriteData;
} }
} }
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function CredentialsOverwrites(
credentialTypes?: ICredentialTypes,
): CredentialsOverwritesClass {
if (!credentialsOverwritesInstance) {
if (credentialTypes) {
credentialsOverwritesInstance = new CredentialsOverwritesClass(credentialTypes);
} else {
throw new Error('CredentialsOverwrites not initialized yet');
}
}
return credentialsOverwritesInstance;
}

View file

@ -1,5 +1,8 @@
import uniq from 'lodash/uniq';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { Container, Service } from 'typedi';
import path from 'path';
import fsPromises from 'fs/promises';
import type { DirectoryLoader, Types } from 'n8n-core'; import type { DirectoryLoader, Types } from 'n8n-core';
import { import {
CUSTOM_EXTENSION_ENV, CUSTOM_EXTENSION_ENV,
@ -9,36 +12,30 @@ import {
LazyPackageDirectoryLoader, LazyPackageDirectoryLoader,
} from 'n8n-core'; } from 'n8n-core';
import type { import type {
ICredentialTypes,
ILogger,
INodesAndCredentials,
KnownNodesAndCredentials, KnownNodesAndCredentials,
INodeTypeDescription, INodeTypeDescription,
LoadedNodesAndCredentials, INodeTypeData,
ICredentialTypeData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';
import config from '@/config'; import config from '@/config';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import { CommunityPackageService } from './services/communityPackage.service';
import { import {
GENERATED_STATIC_DIR,
RESPONSE_ERROR_MESSAGES,
CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_KEY,
CUSTOM_API_CALL_NAME, CUSTOM_API_CALL_NAME,
inTest, inTest,
CLI_DIR, CLI_DIR,
inE2ETests, inE2ETests,
} from '@/constants'; } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import Container, { Service } from 'typedi'; interface LoadedNodesAndCredentials {
nodes: INodeTypeData;
credentials: ICredentialTypeData;
}
@Service() @Service()
export class LoadNodesAndCredentials implements INodesAndCredentials { export class LoadNodesAndCredentials {
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} }; private known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} }; loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
@ -50,20 +47,20 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
includeNodes = config.getEnv('nodes.include'); includeNodes = config.getEnv('nodes.include');
credentialTypes: ICredentialTypes;
logger: ILogger;
private downloadFolder: string; private downloadFolder: string;
private postProcessors: Array<() => Promise<void>> = [];
async init() { async init() {
if (inTest) throw new Error('Not available in tests');
// Make sure the imported modules can resolve dependencies fine. // Make sure the imported modules can resolve dependencies fine.
const delimiter = process.platform === 'win32' ? ';' : ':'; const delimiter = process.platform === 'win32' ? ';' : ':';
process.env.NODE_PATH = module.paths.join(delimiter); process.env.NODE_PATH = module.paths.join(delimiter);
// @ts-ignore // @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (!inTest) module.constructor._initPaths(); module.constructor._initPaths();
if (!inE2ETests) { if (!inE2ETests) {
this.excludeNodes = this.excludeNodes ?? []; this.excludeNodes = this.excludeNodes ?? [];
@ -91,48 +88,30 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
await this.loadNodesFromCustomDirectories(); await this.loadNodesFromCustomDirectories();
await this.postProcessLoaders(); await this.postProcessLoaders();
this.injectCustomApiCallOptions();
} }
async generateTypesForFrontend() { addPostProcessor(fn: () => Promise<void>) {
const credentialsOverwrites = CredentialsOverwrites().getAll(); this.postProcessors.push(fn);
for (const credential of this.types.credentials) {
const overwrittenProperties = [];
this.credentialTypes
.getParentTypes(credential.name)
.reverse()
.map((name) => credentialsOverwrites[name])
.forEach((overwrite) => {
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
});
if (credential.name in credentialsOverwrites) {
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
} }
if (overwrittenProperties.length) { isKnownNode(type: string) {
credential.__overwrittenProperties = uniq(overwrittenProperties); return type in this.known.nodes;
}
} }
// pre-render all the node and credential types as static json files get loadedCredentials() {
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true }); return this.loaded.credentials;
}
const writeStaticJSON = async (name: string, data: object[]) => { get loadedNodes() {
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`); return this.loaded.nodes;
const stream = createWriteStream(filePath, 'utf-8'); }
stream.write('[\n');
data.forEach((entry, index) => {
stream.write(JSON.stringify(entry));
if (index !== data.length - 1) stream.write(',');
stream.write('\n');
});
stream.write(']\n');
stream.end();
};
await writeStaticJSON('nodes', this.types.nodes); get knownCredentials() {
await writeStaticJSON('credentials', this.types.credentials); return this.known.credentials;
}
get knownNodes() {
return this.known.nodes;
} }
private async loadNodesFromNodeModules( private async loadNodesFromNodeModules(
@ -163,6 +142,18 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
} }
} }
resolveIcon(packageName: string, url: string): string | undefined {
const loader = this.loaders[packageName];
if (loader) {
const pathPrefix = `/icons/${packageName}/`;
const filePath = path.resolve(loader.directory, url.substring(pathPrefix.length));
if (!path.relative(loader.directory, filePath).includes('..')) {
return filePath;
}
}
return undefined;
}
getCustomDirectories(): string[] { getCustomDirectories(): string[] {
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()]; const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
@ -180,93 +171,16 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
} }
} }
private async installOrUpdateNpmModule( async loadPackage(packageName: string) {
packageName: string,
options: { version?: string } | { installedPackage: InstalledPackages },
) {
const isUpdate = 'installedPackage' in options;
const command = isUpdate
? `npm update ${packageName}`
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
const communityPackageService = Container.get(CommunityPackageService);
try {
await communityPackageService.executeNpmCommand(command);
} catch (error) {
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`);
}
throw error;
}
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName); const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
let loader: PackageDirectoryLoader;
try {
loader = await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
} catch (error) {
// Remove this package since loading it failed
const removeCommand = `npm remove ${packageName}`;
try {
await communityPackageService.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
} }
if (loader.loadedNodes.length > 0) { async unloadPackage(packageName: string) {
// Save info to DB
try {
if (isUpdate) {
await communityPackageService.removePackageFromDatabase(options.installedPackage);
}
const installedPackage = await communityPackageService.persistInstalledPackage(loader);
await this.postProcessLoaders();
await this.generateTypesForFrontend();
return installedPackage;
} catch (error) {
LoggerProxy.error('Failed to save installed packages and nodes', {
error: error as Error,
packageName,
});
throw error;
}
} else {
// Remove this package since it contains no loadable nodes
const removeCommand = `npm remove ${packageName}`;
try {
await communityPackageService.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
}
}
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
return this.installOrUpdateNpmModule(packageName, { version });
}
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
const communityPackageService = Container.get(CommunityPackageService);
await communityPackageService.executeNpmCommand(`npm remove ${packageName}`);
await communityPackageService.removePackageFromDatabase(installedPackage);
if (packageName in this.loaders) { if (packageName in this.loaders) {
this.loaders[packageName].reset(); this.loaders[packageName].reset();
delete this.loaders[packageName]; delete this.loaders[packageName];
} }
await this.postProcessLoaders();
await this.generateTypesForFrontend();
}
async updateNpmModule(
packageName: string,
installedPackage: InstalledPackages,
): Promise<InstalledPackages> {
return this.installOrUpdateNpmModule(packageName, { installedPackage });
} }
/** /**
@ -382,5 +296,49 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
}; };
} }
} }
this.injectCustomApiCallOptions();
for (const postProcessor of this.postProcessors) {
await postProcessor();
}
}
async setupHotReload() {
const { default: debounce } = await import('lodash/debounce');
// eslint-disable-next-line import/no-extraneous-dependencies
const { watch } = await import('chokidar');
// eslint-disable-next-line @typescript-eslint/naming-convention
const { Push } = await import('@/push');
const push = Container.get(Push);
Object.values(this.loaders).forEach(async (loader) => {
try {
await fsPromises.access(loader.directory);
} catch {
// If directory doesn't exist, there is nothing to watch
return;
}
const realModulePath = path.join(await fsPromises.realpath(loader.directory), path.sep);
const reloader = debounce(async () => {
const modulesToUnload = Object.keys(require.cache).filter((filePath) =>
filePath.startsWith(realModulePath),
);
modulesToUnload.forEach((filePath) => {
delete require.cache[filePath];
});
loader.reset();
await loader.loadAll();
await this.postProcessLoaders();
push.send('nodeDescriptionUpdated', undefined);
}, 100);
const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json'];
watch(toWatch, { cwd: realModulePath }).on('change', reloader);
});
} }
} }

View file

@ -16,12 +16,8 @@ import type { Dirent } from 'fs';
@Service() @Service()
export class NodeTypes implements INodeTypes { export class NodeTypes implements INodeTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {} constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) {
loadNodesAndCredentials.addPostProcessor(async () => this.applySpecialNodeParameters());
init() {
// Some nodeTypes need to get special parameters applied like the
// polling nodes the polling times
this.applySpecialNodeParameters();
} }
/** /**
@ -50,20 +46,20 @@ export class NodeTypes implements INodeTypes {
return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version); return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version);
} }
/* Some nodeTypes need to get special parameters applied like the polling nodes the polling times */
applySpecialNodeParameters() { applySpecialNodeParameters() {
for (const nodeTypeData of Object.values(this.loadedNodes)) { for (const nodeTypeData of Object.values(this.loadNodesAndCredentials.loadedNodes)) {
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type); const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
NodeHelpers.applySpecialNodeParameters(nodeType); NodeHelpers.applySpecialNodeParameters(nodeType);
} }
} }
private getNode(type: string): LoadedClass<INodeType | IVersionedNodeType> { private getNode(type: string): LoadedClass<INodeType | IVersionedNodeType> {
const loadedNodes = this.loadedNodes; const { loadedNodes, knownNodes } = this.loadNodesAndCredentials;
if (type in loadedNodes) { if (type in loadedNodes) {
return loadedNodes[type]; return loadedNodes[type];
} }
const knownNodes = this.knownNodes;
if (type in knownNodes) { if (type in knownNodes) {
const { className, sourcePath } = knownNodes[type]; const { className, sourcePath } = knownNodes[type];
const loaded: INodeType = loadClassInIsolation(sourcePath, className); const loaded: INodeType = loadClassInIsolation(sourcePath, className);
@ -74,14 +70,6 @@ export class NodeTypes implements INodeTypes {
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${type}`); throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${type}`);
} }
private get loadedNodes() {
return this.nodesAndCredentials.loaded.nodes;
}
private get knownNodes() {
return this.nodesAndCredentials.known.nodes;
}
async getNodeTranslationPath({ async getNodeTranslationPath({
nodeSourcePath, nodeSourcePath,
longNodeType, longNodeType,

View file

@ -1,47 +0,0 @@
import path from 'path';
import { realpath, access } from 'fs/promises';
import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { NodeTypes } from '@/NodeTypes';
import type { Push } from '@/push';
export const reloadNodesAndCredentials = async (
loadNodesAndCredentials: LoadNodesAndCredentials,
nodeTypes: NodeTypes,
push: Push,
) => {
const { default: debounce } = await import('lodash/debounce');
// eslint-disable-next-line import/no-extraneous-dependencies
const { watch } = await import('chokidar');
Object.values(loadNodesAndCredentials.loaders).forEach(async (loader) => {
try {
await access(loader.directory);
} catch {
// If directory doesn't exist, there is nothing to watch
return;
}
const realModulePath = path.join(await realpath(loader.directory), path.sep);
const reloader = debounce(async () => {
const modulesToUnload = Object.keys(require.cache).filter((filePath) =>
filePath.startsWith(realModulePath),
);
modulesToUnload.forEach((filePath) => {
delete require.cache[filePath];
});
loader.reset();
await loader.loadAll();
await loadNodesAndCredentials.postProcessLoaders();
await loadNodesAndCredentials.generateTypesForFrontend();
nodeTypes.applySpecialNodeParameters();
push.send('nodeDescriptionUpdated', undefined);
}, 100);
const toWatch = loader.isLazyLoaded
? ['**/nodes.json', '**/credentials.json']
: ['**/*.js', '**/*.json'];
watch(toWatch, { cwd: realModulePath }).on('change', reloader);
});
};

View file

@ -11,7 +11,7 @@ import assert from 'assert';
import { exec as callbackExec } from 'child_process'; import { exec as callbackExec } from 'child_process';
import { access as fsAccess } from 'fs/promises'; import { access as fsAccess } from 'fs/promises';
import os from 'os'; import os from 'os';
import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from 'path'; import { join as pathJoin, resolve as pathResolve } from 'path';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { promisify } from 'util'; import { promisify } from 'util';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
@ -86,7 +86,6 @@ import {
LdapController, LdapController,
MeController, MeController,
MFAController, MFAController,
NodesController,
NodeTypesController, NodeTypesController,
OwnerController, OwnerController,
PasswordResetController, PasswordResetController,
@ -172,6 +171,7 @@ import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { TOTPService } from './Mfa/totp.service'; import { TOTPService } from './Mfa/totp.service';
import { MfaService } from './Mfa/mfa.service'; import { MfaService } from './Mfa/mfa.service';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
import type { FrontendService } from './services/frontend.service';
import { JwtService } from './services/jwt.service'; import { JwtService } from './services/jwt.service';
import { RoleService } from './services/role.service'; import { RoleService } from './services/role.service';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
@ -202,6 +202,8 @@ export class Server extends AbstractServer {
credentialTypes: ICredentialTypes; credentialTypes: ICredentialTypes;
frontendService: FrontendService;
postHog: PostHogClient; postHog: PostHogClient;
push: Push; push: Push;
@ -362,6 +364,16 @@ export class Server extends AbstractServer {
this.credentialTypes = Container.get(CredentialTypes); this.credentialTypes = Container.get(CredentialTypes);
this.nodeTypes = Container.get(NodeTypes); this.nodeTypes = Container.get(NodeTypes);
if (!config.getEnv('endpoints.disableUi')) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { FrontendService } = await import('@/services/frontend.service');
this.frontendService = Container.get(FrontendService);
this.loadNodesAndCredentials.addPostProcessor(async () =>
this.frontendService.generateTypes(),
);
await this.frontendService.generateTypes();
}
this.activeExecutionsInstance = Container.get(ActiveExecutions); this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = Container.get(WaitTracker); this.waitTracker = Container.get(WaitTracker);
this.postHog = Container.get(PostHogClient); this.postHog = Container.get(PostHogClient);
@ -419,8 +431,7 @@ export class Server extends AbstractServer {
}; };
if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') { if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') {
const { reloadNodesAndCredentials } = await import('@/ReloadNodesAndCredentials'); void this.loadNodesAndCredentials.setupHotReload();
await reloadNodesAndCredentials(this.loadNodesAndCredentials, this.nodeTypes, this.push);
} }
void Db.collections.Workflow.findOne({ void Db.collections.Workflow.findOne({
@ -435,7 +446,7 @@ export class Server extends AbstractServer {
/** /**
* Returns the current settings for the frontend * Returns the current settings for the frontend
*/ */
getSettingsForFrontend(): IN8nUISettings { private async getSettingsForFrontend(): Promise<IN8nUISettings> {
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
const instanceBaseUrl = getInstanceBaseUrl(); const instanceBaseUrl = getInstanceBaseUrl();
this.frontendSettings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); this.frontendSettings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
@ -506,8 +517,11 @@ export class Server extends AbstractServer {
}); });
} }
if (config.get('nodes.packagesMissing').length > 0) { if (config.getEnv('nodes.communityPackages.enabled')) {
this.frontendSettings.missingPackages = true; // eslint-disable-next-line @typescript-eslint/naming-convention
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
this.frontendSettings.missingPackages =
Container.get(CommunityPackagesService).hasMissingPackages;
} }
this.frontendSettings.mfa.enabled = isMfaFeatureEnabled(); this.frontendSettings.mfa.enabled = isMfaFeatureEnabled();
@ -585,9 +599,11 @@ export class Server extends AbstractServer {
} }
if (config.getEnv('nodes.communityPackages.enabled')) { if (config.getEnv('nodes.communityPackages.enabled')) {
controllers.push( // eslint-disable-next-line @typescript-eslint/naming-convention
new NodesController(config, this.loadNodesAndCredentials, this.push, internalHooks), const { CommunityPackagesController } = await import(
'@/controllers/communityPackages.controller'
); );
controllers.push(Container.get(CommunityPackagesController));
} }
if (inE2ETests) { if (inE2ETests) {
@ -1480,9 +1496,9 @@ export class Server extends AbstractServer {
return; return;
} }
CredentialsOverwrites().setData(body); Container.get(CredentialsOverwrites).setData(body);
await this.loadNodesAndCredentials.generateTypesForFrontend(); await this.frontendService?.generateTypes();
this.presetCredentialsLoaded = true; this.presetCredentialsLoaded = true;
@ -1509,22 +1525,13 @@ export class Server extends AbstractServer {
const serveIcons: express.RequestHandler = async (req, res) => { const serveIcons: express.RequestHandler = async (req, res) => {
let { scope, packageName } = req.params; let { scope, packageName } = req.params;
if (scope) packageName = `@${scope}/${packageName}`; if (scope) packageName = `@${scope}/${packageName}`;
const loader = this.loadNodesAndCredentials.loaders[packageName]; const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl);
if (loader) { if (filePath) {
const pathPrefix = `/icons/${packageName}/`;
const filePath = pathResolve(
loader.directory,
req.originalUrl.substring(pathPrefix.length),
);
if (pathRelative(loader.directory, filePath).includes('..')) {
return res.status(404).end();
}
try { try {
await fsAccess(filePath); await fsAccess(filePath);
return res.sendFile(filePath); return res.sendFile(filePath);
} catch {} } catch {}
} }
res.sendStatus(404); res.sendStatus(404);
}; };

View file

@ -35,8 +35,6 @@ import {
WorkflowHooks, WorkflowHooks,
WorkflowOperationError, WorkflowOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import type { import type {
@ -115,8 +113,6 @@ class WorkflowRunnerProcess {
await loadNodesAndCredentials.init(); await loadNodesAndCredentials.init();
const nodeTypes = Container.get(NodeTypes); const nodeTypes = Container.get(NodeTypes);
const credentialTypes = Container.get(CredentialTypes);
CredentialsOverwrites(credentialTypes);
// Load all external hooks // Load all external hooks
const externalHooks = Container.get(ExternalHooks); const externalHooks = Container.get(ExternalHooks);

View file

@ -1,8 +1,9 @@
import * as path from 'path'; import * as path from 'path';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { Container } from 'typedi';
import config from '@/config';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { getNodeTypes } from '@/audit/utils'; import { getNodeTypes } from '@/audit/utils';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { import {
OFFICIAL_RISKY_NODE_TYPES, OFFICIAL_RISKY_NODE_TYPES,
ENV_VARS_DOCS_URL, ENV_VARS_DOCS_URL,
@ -12,10 +13,13 @@ import {
} from '@/audit/constants'; } from '@/audit/constants';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { Risk } from '@/audit/types'; import type { Risk } from '@/audit/types';
import { Container } from 'typedi';
async function getCommunityNodeDetails() { async function getCommunityNodeDetails() {
const installedPackages = await Container.get(CommunityPackageService).getAllInstalledPackages(); if (!config.getEnv('nodes.communityPackages.enabled')) return [];
// eslint-disable-next-line @typescript-eslint/naming-convention
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
const installedPackages = await Container.get(CommunityPackagesService).getAllInstalledPackages();
return installedPackages.reduce<Risk.CommunityNodeDetails[]>((acc, pkg) => { return installedPackages.reduce<Risk.CommunityNodeDetails[]>((acc, pkg) => {
pkg.installedNodes.forEach((node) => pkg.installedNodes.forEach((node) =>

View file

@ -10,8 +10,6 @@ import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as CrashJournal from '@/CrashJournal'; import * as CrashJournal from '@/CrashJournal';
import { LICENSE_FEATURES, inTest } from '@/constants'; import { LICENSE_FEATURES, inTest } from '@/constants';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { initErrorHandling } from '@/ErrorReporting'; import { initErrorHandling } from '@/ErrorReporting';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@ -30,8 +28,6 @@ export abstract class BaseCommand extends Command {
protected externalHooks: IExternalHooksClass; protected externalHooks: IExternalHooksClass;
protected loadNodesAndCredentials: LoadNodesAndCredentials;
protected nodeTypes: NodeTypes; protected nodeTypes: NodeTypes;
protected userSettings: IUserSettings; protected userSettings: IUserSettings;
@ -54,12 +50,8 @@ export abstract class BaseCommand extends Command {
// Make sure the settings exist // Make sure the settings exist
this.userSettings = await UserSettings.prepareUserSettings(); this.userSettings = await UserSettings.prepareUserSettings();
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials); await Container.get(LoadNodesAndCredentials).init();
await this.loadNodesAndCredentials.init();
this.nodeTypes = Container.get(NodeTypes); this.nodeTypes = Container.get(NodeTypes);
this.nodeTypes.init();
const credentialTypes = Container.get(CredentialTypes);
CredentialsOverwrites(credentialTypes);
await Db.init().catch(async (error: Error) => await Db.init().catch(async (error: Error) =>
this.exitWithCrash('There was an error initializing DB', error), this.exitWithCrash('There was an error initializing DB', error),

View file

@ -23,7 +23,6 @@ import * as Db from '@/Db';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { Server } from '@/Server'; import { Server } from '@/Server';
import { TestWebhooks } from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
import { CommunityPackageService } from '@/services/communityPackage.service';
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants'; import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
import { eventBus } from '@/eventbus'; import { eventBus } from '@/eventbus';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
@ -257,8 +256,6 @@ export class Start extends BaseCommand {
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex')); config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
} }
await this.loadNodesAndCredentials.generateTypesForFrontend();
await UserSettings.getEncryptionKey(); await UserSettings.getEncryptionKey();
// Load settings from database and set them to config. // Load settings from database and set them to config.
@ -270,12 +267,11 @@ export class Start extends BaseCommand {
const areCommunityPackagesEnabled = config.getEnv('nodes.communityPackages.enabled'); const areCommunityPackagesEnabled = config.getEnv('nodes.communityPackages.enabled');
if (areCommunityPackagesEnabled) { if (areCommunityPackagesEnabled) {
await Container.get(CommunityPackageService).setMissingPackages( // eslint-disable-next-line @typescript-eslint/naming-convention
this.loadNodesAndCredentials, const { CommunityPackagesService } = await import('@/services/communityPackages.service');
{ await Container.get(CommunityPackagesService).setMissingPackages({
reinstallMissingPackages: flags.reinstallMissingPackages, reinstallMissingPackages: flags.reinstallMissingPackages,
}, });
);
} }
const dbType = config.getEnv('database.type'); const dbType = config.getEnv('database.type');

View file

@ -463,7 +463,7 @@ export class Worker extends BaseCommand {
return; return;
} }
CredentialsOverwrites().setData(body); Container.get(CredentialsOverwrites).setData(body);
presetCredentialsLoaded = true; presetCredentialsLoaded = true;
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
} else { } else {

View file

@ -818,12 +818,6 @@ export const schema = {
env: 'N8N_COMMUNITY_PACKAGES_ENABLED', env: 'N8N_COMMUNITY_PACKAGES_ENABLED',
}, },
}, },
packagesMissing: {
// Used to have a persistent list of packages
doc: 'Contains a comma separated list of packages that failed to load during startup',
format: String,
default: '',
},
}, },
logs: { logs: {

View file

@ -1,4 +1,6 @@
import { Service } from 'typedi';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import config from '@/config';
import { import {
RESPONSE_ERROR_MESSAGES, RESPONSE_ERROR_MESSAGES,
STARTER_TEMPLATE_NAME, STARTER_TEMPLATE_NAME,
@ -9,12 +11,9 @@ import { NodeRequest } from '@/requests';
import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { BadRequestError, InternalServerError } from '@/ResponseHelper';
import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces'; import type { CommunityPackages } from '@/Interfaces';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { Push } from '@/push'; import { Push } from '@/push';
import { Config } from '@/config'; import { CommunityPackagesService } from '@/services/communityPackages.service';
import { CommunityPackageService } from '@/services/communityPackage.service';
import Container from 'typedi';
const { const {
PACKAGE_NOT_INSTALLED, PACKAGE_NOT_INSTALLED,
@ -33,24 +32,20 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error; return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
} }
@Service()
@Authorized(['global', 'owner']) @Authorized(['global', 'owner'])
@RestController('/nodes') @RestController('/community-packages')
export class NodesController { export class CommunityPackagesController {
private communityPackageService: CommunityPackageService;
constructor( constructor(
private config: Config,
private loadNodesAndCredentials: LoadNodesAndCredentials,
private push: Push, private push: Push,
private internalHooks: InternalHooks, private internalHooks: InternalHooks,
) { private communityPackagesService: CommunityPackagesService,
this.communityPackageService = Container.get(CommunityPackageService); ) {}
}
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
@Middleware() @Middleware()
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) { checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
if (this.config.getEnv('executions.mode') === 'queue' && req.method !== 'GET') if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
res.status(400).json({ res.status(400).json({
status: 'error', status: 'error',
message: 'Package management is disabled when running in "queue" mode', message: 'Package management is disabled when running in "queue" mode',
@ -69,7 +64,7 @@ export class NodesController {
let parsed: CommunityPackages.ParsedPackageName; let parsed: CommunityPackages.ParsedPackageName;
try { try {
parsed = this.communityPackageService.parseNpmPackageName(name); parsed = this.communityPackagesService.parseNpmPackageName(name);
} catch (error) { } catch (error) {
throw new BadRequestError( throw new BadRequestError(
error instanceof Error ? error.message : 'Failed to parse package name', error instanceof Error ? error.message : 'Failed to parse package name',
@ -85,8 +80,8 @@ export class NodesController {
); );
} }
const isInstalled = await this.communityPackageService.isPackageInstalled(parsed.packageName); const isInstalled = await this.communityPackagesService.isPackageInstalled(parsed.packageName);
const hasLoaded = this.communityPackageService.hasPackageLoaded(name); const hasLoaded = this.communityPackagesService.hasPackageLoaded(name);
if (isInstalled && hasLoaded) { if (isInstalled && hasLoaded) {
throw new BadRequestError( throw new BadRequestError(
@ -97,7 +92,7 @@ export class NodesController {
); );
} }
const packageStatus = await this.communityPackageService.checkNpmPackageStatus(name); const packageStatus = await this.communityPackagesService.checkNpmPackageStatus(name);
if (packageStatus.status !== 'OK') { if (packageStatus.status !== 'OK') {
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`); throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`);
@ -105,7 +100,7 @@ export class NodesController {
let installedPackage: InstalledPackages; let installedPackage: InstalledPackages;
try { try {
installedPackage = await this.loadNodesAndCredentials.installNpmModule( installedPackage = await this.communityPackagesService.installNpmModule(
parsed.packageName, parsed.packageName,
parsed.version, parsed.version,
); );
@ -130,7 +125,7 @@ export class NodesController {
throw new (clientError ? BadRequestError : InternalServerError)(message); throw new (clientError ? BadRequestError : InternalServerError)(message);
} }
if (!hasLoaded) this.communityPackageService.removePackageFromMissingList(name); if (!hasLoaded) this.communityPackagesService.removePackageFromMissingList(name);
// broadcast to connected frontends that node list has been updated // broadcast to connected frontends that node list has been updated
installedPackage.installedNodes.forEach((node) => { installedPackage.installedNodes.forEach((node) => {
@ -156,7 +151,7 @@ export class NodesController {
@Get('/') @Get('/')
async getInstalledPackages() { async getInstalledPackages() {
const installedPackages = await this.communityPackageService.getAllInstalledPackages(); const installedPackages = await this.communityPackagesService.getAllInstalledPackages();
if (installedPackages.length === 0) return []; if (installedPackages.length === 0) return [];
@ -164,7 +159,7 @@ export class NodesController {
try { try {
const command = ['npm', 'outdated', '--json'].join(' '); const command = ['npm', 'outdated', '--json'].join(' ');
await this.communityPackageService.executeNpmCommand(command, { doNotHandleError: true }); await this.communityPackagesService.executeNpmCommand(command, { doNotHandleError: true });
} catch (error) { } catch (error) {
// when there are updates, npm exits with code 1 // when there are updates, npm exits with code 1
// when there are no updates, command succeeds // when there are no updates, command succeeds
@ -174,18 +169,14 @@ export class NodesController {
} }
} }
let hydratedPackages = this.communityPackageService.matchPackagesWithUpdates( let hydratedPackages = this.communityPackagesService.matchPackagesWithUpdates(
installedPackages, installedPackages,
pendingUpdates, pendingUpdates,
); );
try { try {
const missingPackages = this.config.get('nodes.packagesMissing') as string | undefined; if (this.communityPackagesService.hasMissingPackages) {
if (missingPackages) { hydratedPackages = this.communityPackagesService.matchMissingPackages(hydratedPackages);
hydratedPackages = this.communityPackageService.matchMissingPackages(
hydratedPackages,
missingPackages,
);
} }
} catch {} } catch {}
@ -201,21 +192,21 @@ export class NodesController {
} }
try { try {
this.communityPackageService.parseNpmPackageName(name); // sanitize input this.communityPackagesService.parseNpmPackageName(name); // sanitize input
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON; const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
throw new BadRequestError(message); throw new BadRequestError(message);
} }
const installedPackage = await this.communityPackageService.findInstalledPackage(name); const installedPackage = await this.communityPackagesService.findInstalledPackage(name);
if (!installedPackage) { if (!installedPackage) {
throw new BadRequestError(PACKAGE_NOT_INSTALLED); throw new BadRequestError(PACKAGE_NOT_INSTALLED);
} }
try { try {
await this.loadNodesAndCredentials.removeNpmModule(name, installedPackage); await this.communityPackagesService.removeNpmModule(name, installedPackage);
} catch (error) { } catch (error) {
const message = [ const message = [
`Error removing package "${name}"`, `Error removing package "${name}"`,
@ -252,15 +243,15 @@ export class NodesController {
} }
const previouslyInstalledPackage = const previouslyInstalledPackage =
await this.communityPackageService.findInstalledPackage(name); await this.communityPackagesService.findInstalledPackage(name);
if (!previouslyInstalledPackage) { if (!previouslyInstalledPackage) {
throw new BadRequestError(PACKAGE_NOT_INSTALLED); throw new BadRequestError(PACKAGE_NOT_INSTALLED);
} }
try { try {
const newInstalledPackage = await this.loadNodesAndCredentials.updateNpmModule( const newInstalledPackage = await this.communityPackagesService.updateNpmModule(
this.communityPackageService.parseNpmPackageName(name).packageName, this.communityPackagesService.parseNpmPackageName(name).packageName,
previouslyInstalledPackage, previouslyInstalledPackage,
); );

View file

@ -2,7 +2,6 @@ export { AuthController } from './auth.controller';
export { LdapController } from './ldap.controller'; export { LdapController } from './ldap.controller';
export { MeController } from './me.controller'; export { MeController } from './me.controller';
export { MFAController } from './mfa.controller'; export { MFAController } from './mfa.controller';
export { NodesController } from './nodes.controller';
export { NodeTypesController } from './nodeTypes.controller'; export { NodeTypesController } from './nodeTypes.controller';
export { OwnerController } from './owner.controller'; export { OwnerController } from './owner.controller';
export { PasswordResetController } from './passwordReset.controller'; export { PasswordResetController } from './passwordReset.controller';

View file

@ -1,13 +1,14 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
import { LoggerProxy as Logger } from 'n8n-workflow';
import { UserSettings } from 'n8n-core';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { promisify } from 'util'; import { promisify } from 'util';
import axios from 'axios'; import axios from 'axios';
import config from '@/config'; import { LoggerProxy as Logger } from 'n8n-workflow';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { UserSettings } from 'n8n-core';
import type { PackageDirectoryLoader } from 'n8n-core';
import { toError } from '@/utils'; import { toError } from '@/utils';
import { InstalledPackagesRepository } from '@/databases/repositories/installedPackages.repository'; import { InstalledPackagesRepository } from '@/databases/repositories/installedPackages.repository';
import type { InstalledPackages } from '@/databases/entities/InstalledPackages'; import type { InstalledPackages } from '@/databases/entities/InstalledPackages';
@ -18,11 +19,8 @@ import {
RESPONSE_ERROR_MESSAGES, RESPONSE_ERROR_MESSAGES,
UNKNOWN_FAILURE_REASON, UNKNOWN_FAILURE_REASON,
} from '@/constants'; } from '@/constants';
import type { PublicInstalledPackage } from 'n8n-workflow';
import type { PackageDirectoryLoader } from 'n8n-core';
import type { CommunityPackages } from '@/Interfaces'; import type { CommunityPackages } from '@/Interfaces';
import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
const { const {
PACKAGE_NAME_NOT_PROVIDED, PACKAGE_NAME_NOT_PROVIDED,
@ -45,8 +43,17 @@ const asyncExec = promisify(exec);
const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/; const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
@Service() @Service()
export class CommunityPackageService { export class CommunityPackagesService {
constructor(private readonly installedPackageRepository: InstalledPackagesRepository) {} missingPackages: string[] = [];
constructor(
private readonly installedPackageRepository: InstalledPackagesRepository,
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
) {}
get hasMissingPackages() {
return this.missingPackages.length > 0;
}
async findInstalledPackage(packageName: string) { async findInstalledPackage(packageName: string) {
return this.installedPackageRepository.findOne({ return this.installedPackageRepository.findOne({
@ -173,9 +180,8 @@ export class CommunityPackageService {
}, []); }, []);
} }
matchMissingPackages(installedPackages: PublicInstalledPackage[], missingPackages: string) { matchMissingPackages(installedPackages: PublicInstalledPackage[]) {
const missingPackagesList = missingPackages const missingPackagesList = this.missingPackages
.split(' ')
.map((name) => { .map((name) => {
try { try {
// Strip away versions but maintain scope and package name // Strip away versions but maintain scope and package name
@ -221,13 +227,9 @@ export class CommunityPackageService {
} }
hasPackageLoaded(packageName: string) { hasPackageLoaded(packageName: string) {
const missingPackages = config.get('nodes.packagesMissing') as string | undefined; if (!this.missingPackages.length) return true;
if (!missingPackages) return true; return !this.missingPackages.some(
return !missingPackages
.split(' ')
.some(
(packageNameAndVersion) => (packageNameAndVersion) =>
packageNameAndVersion.startsWith(packageName) && packageNameAndVersion.startsWith(packageName) &&
packageNameAndVersion.replace(packageName, '').startsWith('@'), packageNameAndVersion.replace(packageName, '').startsWith('@'),
@ -236,30 +238,23 @@ export class CommunityPackageService {
removePackageFromMissingList(packageName: string) { removePackageFromMissingList(packageName: string) {
try { try {
const failedPackages = config.get('nodes.packagesMissing').split(' '); this.missingPackages = this.missingPackages.filter(
const packageFailedToLoad = failedPackages.filter(
(packageNameAndVersion) => (packageNameAndVersion) =>
!packageNameAndVersion.startsWith(packageName) || !packageNameAndVersion.startsWith(packageName) ||
!packageNameAndVersion.replace(packageName, '').startsWith('@'), !packageNameAndVersion.replace(packageName, '').startsWith('@'),
); );
config.set('nodes.packagesMissing', packageFailedToLoad.join(' '));
} catch { } catch {
// do nothing // do nothing
} }
} }
async setMissingPackages( async setMissingPackages({ reinstallMissingPackages }: { reinstallMissingPackages: boolean }) {
loadNodesAndCredentials: LoadNodesAndCredentials,
{ reinstallMissingPackages }: { reinstallMissingPackages: boolean },
) {
const installedPackages = await this.getAllInstalledPackages(); const installedPackages = await this.getAllInstalledPackages();
const missingPackages = new Set<{ packageName: string; version: string }>(); const missingPackages = new Set<{ packageName: string; version: string }>();
installedPackages.forEach((installedPackage) => { installedPackages.forEach((installedPackage) => {
installedPackage.installedNodes.forEach((installedNode) => { installedPackage.installedNodes.forEach((installedNode) => {
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) { if (!this.loadNodesAndCredentials.isKnownNode(installedNode.type)) {
// Leave the list ready for installing in case we need. // Leave the list ready for installing in case we need.
missingPackages.add({ missingPackages.add({
packageName: installedPackage.packageName, packageName: installedPackage.packageName,
@ -269,7 +264,7 @@ export class CommunityPackageService {
}); });
}); });
config.set('nodes.packagesMissing', ''); this.missingPackages = [];
if (missingPackages.size === 0) return; if (missingPackages.size === 0) return;
@ -283,10 +278,7 @@ export class CommunityPackageService {
// Optimistic approach - stop if any installation fails // Optimistic approach - stop if any installation fails
for (const missingPackage of missingPackages) { for (const missingPackage of missingPackages) {
await loadNodesAndCredentials.installNpmModule( await this.installNpmModule(missingPackage.packageName, missingPackage.version);
missingPackage.packageName,
missingPackage.version,
);
missingPackages.delete(missingPackage); missingPackages.delete(missingPackage);
} }
@ -296,11 +288,79 @@ export class CommunityPackageService {
} }
} }
config.set( this.missingPackages = [...missingPackages].map(
'nodes.packagesMissing', (missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`,
Array.from(missingPackages)
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
.join(' '),
); );
} }
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
return this.installOrUpdateNpmModule(packageName, { version });
}
async updateNpmModule(
packageName: string,
installedPackage: InstalledPackages,
): Promise<InstalledPackages> {
return this.installOrUpdateNpmModule(packageName, { installedPackage });
}
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
await this.executeNpmCommand(`npm remove ${packageName}`);
await this.removePackageFromDatabase(installedPackage);
await this.loadNodesAndCredentials.unloadPackage(packageName);
await this.loadNodesAndCredentials.postProcessLoaders();
}
private async installOrUpdateNpmModule(
packageName: string,
options: { version?: string } | { installedPackage: InstalledPackages },
) {
const isUpdate = 'installedPackage' in options;
const command = isUpdate
? `npm update ${packageName}`
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
try {
await this.executeNpmCommand(command);
} catch (error) {
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`);
}
throw error;
}
let loader: PackageDirectoryLoader;
try {
loader = await this.loadNodesAndCredentials.loadPackage(packageName);
} catch (error) {
// Remove this package since loading it failed
const removeCommand = `npm remove ${packageName}`;
try {
await this.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
}
if (loader.loadedNodes.length > 0) {
// Save info to DB
try {
if (isUpdate) {
await this.removePackageFromDatabase(options.installedPackage);
}
const installedPackage = await this.persistInstalledPackage(loader);
await this.loadNodesAndCredentials.postProcessLoaders();
return installedPackage;
} catch (error) {
throw new Error(`Failed to save installed package: ${packageName}`, { cause: error });
}
} else {
// Remove this package since it contains no loadable nodes
const removeCommand = `npm remove ${packageName}`;
try {
await this.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
}
}
} }

View file

@ -0,0 +1,67 @@
import { Service } from 'typedi';
import uniq from 'lodash/uniq';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';
import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow';
import { GENERATED_STATIC_DIR } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
@Service()
export class FrontendService {
constructor(
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly credentialTypes: CredentialTypes,
private readonly credentialsOverwrites: CredentialsOverwrites,
) {}
async generateTypes() {
this.overwriteCredentialsProperties();
// pre-render all the node and credential types as static json files
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true });
const { credentials, nodes } = this.loadNodesAndCredentials.types;
this.writeStaticJSON('nodes', nodes);
this.writeStaticJSON('credentials', credentials);
}
private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) {
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`);
const stream = createWriteStream(filePath, 'utf-8');
stream.write('[\n');
data.forEach((entry, index) => {
stream.write(JSON.stringify(entry));
if (index !== data.length - 1) stream.write(',');
stream.write('\n');
});
stream.write(']\n');
stream.end();
}
private overwriteCredentialsProperties() {
const { credentials } = this.loadNodesAndCredentials.types;
const credentialsOverwrites = this.credentialsOverwrites.getAll();
for (const credential of credentials) {
const overwrittenProperties = [];
this.credentialTypes
.getParentTypes(credential.name)
.reverse()
.map((name) => credentialsOverwrites[name])
.forEach((overwrite) => {
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
});
if (credential.name in credentialsOverwrites) {
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
}
if (overwrittenProperties.length) {
credential.__overwrittenProperties = uniq(overwrittenProperties);
}
}
}
}

View file

@ -8,7 +8,7 @@ import { toReportTitle } from '@/audit/utils';
import { mockInstance } from '../shared/utils/'; import { mockInstance } from '../shared/utils/';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { CommunityPackageService } from '@/services/communityPackage.service'; import { CommunityPackagesService } from '@/services/communityPackages.service';
import Container from 'typedi'; import Container from 'typedi';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
@ -19,8 +19,8 @@ LoggerProxy.init(getLogger());
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials); const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
nodesAndCredentials.getCustomDirectories.mockReturnValue([]); nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
mockInstance(NodeTypes); mockInstance(NodeTypes);
const communityPackageService = mockInstance(CommunityPackageService); const communityPackagesService = mockInstance(CommunityPackagesService);
Container.set(CommunityPackageService, communityPackageService); Container.set(CommunityPackagesService, communityPackagesService);
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();
@ -36,7 +36,7 @@ afterAll(async () => {
}); });
test('should report risky official nodes', async () => { test('should report risky official nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => { const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => {
return (acc[cur] = uuid()), acc; return (acc[cur] = uuid()), acc;
}, {}); }, {});
@ -81,7 +81,7 @@ test('should report risky official nodes', async () => {
}); });
test('should not report non-risky official nodes', async () => { test('should not report non-risky official nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
await saveManualTriggerWorkflow(); await saveManualTriggerWorkflow();
const testAudit = await audit(['nodes']); const testAudit = await audit(['nodes']);
@ -96,7 +96,7 @@ test('should not report non-risky official nodes', async () => {
}); });
test('should report community nodes', async () => { test('should report community nodes', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
const testAudit = await audit(['nodes']); const testAudit = await audit(['nodes']);

View file

@ -1,16 +1,18 @@
import * as testDb from '../shared/testDb'; import * as Config from '@oclif/config';
import { mockInstance } from '../shared/utils/'; import { mock } from 'jest-mock-extended';
import { type ILogger, LoggerProxy } from 'n8n-workflow';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { ImportWorkflowsCommand } from '@/commands/import/workflow'; import { ImportWorkflowsCommand } from '@/commands/import/workflow';
import * as Config from '@oclif/config'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as testDb from '../shared/testDb';
import { mockInstance } from '../shared/utils/';
import { LoggerProxy } from 'n8n-workflow'; LoggerProxy.init(mock<ILogger>());
import { getLogger } from '@/Logger';
LoggerProxy.init(getLogger());
beforeAll(async () => { beforeAll(async () => {
mockInstance(InternalHooks); mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
await testDb.init(); await testDb.init();
}); });

View file

@ -1,8 +1,11 @@
import path from 'path'; import path from 'path';
import type { SuperAgentTest } from 'supertest';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { Push } from '@/push'; import { Push } from '@/push';
import { CommunityPackageService } from '@/services/communityPackage.service'; import { CommunityPackagesService } from '@/services/communityPackages.service';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
@ -14,15 +17,13 @@ import {
mockPackageName, mockPackageName,
} from './shared/utils'; } from './shared/utils';
import type { InstalledPackages } from '@db/entities/InstalledPackages'; const communityPackagesService = mockInstance(CommunityPackagesService, {
import type { InstalledNodes } from '@db/entities/InstalledNodes'; hasMissingPackages: false,
import type { SuperAgentTest } from 'supertest'; });
mockInstance(LoadNodesAndCredentials);
const communityPackageService = mockInstance(CommunityPackageService);
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
mockInstance(Push); mockInstance(Push);
const testServer = setupTestServer({ endpointGroups: ['nodes'] }); const testServer = setupTestServer({ endpointGroups: ['community-packages'] });
const commonUpdatesProps = { const commonUpdatesProps = {
createdAt: new Date(), createdAt: new Date(),
@ -47,12 +48,12 @@ beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
describe('GET /nodes', () => { describe('GET /community-packages', () => {
test('should respond 200 if no nodes are installed', async () => { test('should respond 200 if no nodes are installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([]); communityPackagesService.getAllInstalledPackages.mockResolvedValue([]);
const { const {
body: { data }, body: { data },
} = await authAgent.get('/nodes').expect(200); } = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(0); expect(data).toHaveLength(0);
}); });
@ -61,12 +62,12 @@ describe('GET /nodes', () => {
const pkg = mockPackage(); const pkg = mockPackage();
const node = mockNode(pkg.packageName); const node = mockNode(pkg.packageName);
pkg.installedNodes = [node]; pkg.installedNodes = [node];
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]); communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]); communityPackagesService.matchPackagesWithUpdates.mockReturnValue([pkg]);
const { const {
body: { data }, body: { data },
} = await authAgent.get('/nodes').expect(200); } = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(1); expect(data).toHaveLength(1);
expect(data[0].installedNodes).toHaveLength(1); expect(data[0].installedNodes).toHaveLength(1);
@ -80,9 +81,9 @@ describe('GET /nodes', () => {
const nodeB = mockNode(pkgB.packageName); const nodeB = mockNode(pkgB.packageName);
const nodeC = mockNode(pkgB.packageName); const nodeC = mockNode(pkgB.packageName);
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]); communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
communityPackageService.matchPackagesWithUpdates.mockReturnValue([ communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
{ {
...commonUpdatesProps, ...commonUpdatesProps,
packageName: pkgA.packageName, packageName: pkgA.packageName,
@ -97,7 +98,7 @@ describe('GET /nodes', () => {
const { const {
body: { data }, body: { data },
} = await authAgent.get('/nodes').expect(200); } = await authAgent.get('/community-packages').expect(200);
expect(data).toHaveLength(2); expect(data).toHaveLength(2);
@ -110,26 +111,26 @@ describe('GET /nodes', () => {
}); });
test('should not check for updates if no packages installed', async () => { test('should not check for updates if no packages installed', async () => {
await authAgent.get('/nodes'); await authAgent.get('/community-packages');
expect(communityPackageService.executeNpmCommand).not.toHaveBeenCalled(); expect(communityPackagesService.executeNpmCommand).not.toHaveBeenCalled();
}); });
test('should check for updates if packages installed', async () => { test('should check for updates if packages installed', async () => {
communityPackageService.getAllInstalledPackages.mockResolvedValue([mockPackage()]); communityPackagesService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
await authAgent.get('/nodes').expect(200); await authAgent.get('/community-packages').expect(200);
const args = ['npm outdated --json', { doNotHandleError: true }]; const args = ['npm outdated --json', { doNotHandleError: true }];
expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args); expect(communityPackagesService.executeNpmCommand).toHaveBeenCalledWith(...args);
}); });
test('should report package updates if available', async () => { test('should report package updates if available', async () => {
const pkg = mockPackage(); const pkg = mockPackage();
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]); communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
communityPackageService.executeNpmCommand.mockImplementation(() => { communityPackagesService.executeNpmCommand.mockImplementation(() => {
throw { throw {
code: 1, code: 1,
stdout: JSON.stringify({ stdout: JSON.stringify({
@ -143,7 +144,7 @@ describe('GET /nodes', () => {
}; };
}); });
communityPackageService.matchPackagesWithUpdates.mockReturnValue([ communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
{ {
packageName: 'test', packageName: 'test',
installedNodes: [], installedNodes: [],
@ -153,7 +154,7 @@ describe('GET /nodes', () => {
const { const {
body: { data }, body: { data },
} = await authAgent.get('/nodes').expect(200); } = await authAgent.get('/community-packages').expect(200);
const [returnedPkg] = data; const [returnedPkg] = data;
@ -162,89 +163,92 @@ describe('GET /nodes', () => {
}); });
}); });
describe('POST /nodes', () => { describe('POST /community-packages', () => {
test('should reject if package name is missing', async () => { test('should reject if package name is missing', async () => {
await authAgent.post('/nodes').expect(400); await authAgent.post('/community-packages').expect(400);
}); });
test('should reject if package is duplicate', async () => { test('should reject if package is duplicate', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.isPackageInstalled.mockResolvedValue(true); communityPackagesService.isPackageInstalled.mockResolvedValue(true);
communityPackageService.hasPackageLoaded.mockReturnValue(true); communityPackagesService.hasPackageLoaded.mockReturnValue(true);
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const { const {
body: { message }, body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400); } = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('already installed'); expect(message).toContain('already installed');
}); });
test('should allow installing packages that could not be loaded', async () => { test('should allow installing packages that could not be loaded', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.hasPackageLoaded.mockReturnValue(false); communityPackagesService.hasPackageLoaded.mockReturnValue(false);
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' }); communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage()); communityPackagesService.installNpmModule.mockResolvedValue(mockPackage());
await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(200); await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200);
expect(communityPackageService.removePackageFromMissingList).toHaveBeenCalled(); expect(communityPackagesService.removePackageFromMissingList).toHaveBeenCalled();
}); });
test('should not install a banned package', async () => { test('should not install a banned package', async () => {
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' }); communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
const { const {
body: { message }, body: { message },
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400); } = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('banned'); expect(message).toContain('banned');
}); });
}); });
describe('DELETE /nodes', () => { describe('DELETE /community-packages', () => {
test('should not delete if package name is empty', async () => { test('should not delete if package name is empty', async () => {
await authAgent.delete('/nodes').expect(400); await authAgent.delete('/community-packages').expect(400);
}); });
test('should reject if package is not installed', async () => { test('should reject if package is not installed', async () => {
const { const {
body: { message }, body: { message },
} = await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(400); } = await authAgent
.delete('/community-packages')
.query({ name: mockPackageName() })
.expect(400);
expect(message).toContain('not installed'); expect(message).toContain('not installed');
}); });
test('should uninstall package', async () => { test('should uninstall package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(200); await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200);
expect(mockLoadNodesAndCredentials.removeNpmModule).toHaveBeenCalledTimes(1); expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1);
}); });
}); });
describe('PATCH /nodes', () => { describe('PATCH /community-packages', () => {
test('should reject if package name is empty', async () => { test('should reject if package name is empty', async () => {
await authAgent.patch('/nodes').expect(400); await authAgent.patch('/community-packages').expect(400);
}); });
test('should reject if package is not installed', async () => { test('should reject if package is not installed', async () => {
const { const {
body: { message }, body: { message },
} = await authAgent.patch('/nodes').send({ name: mockPackageName() }).expect(400); } = await authAgent.patch('/community-packages').send({ name: mockPackageName() }).expect(400);
expect(message).toContain('not installed'); expect(message).toContain('not installed');
}); });
test('should update a package', async () => { test('should update a package', async () => {
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
await authAgent.patch('/nodes').send({ name: mockPackageName() }); await authAgent.patch('/community-packages').send({ name: mockPackageName() });
expect(mockLoadNodesAndCredentials.updateNpmModule).toHaveBeenCalledTimes(1); expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1);
}); });
}); });

View file

@ -22,7 +22,7 @@ export type EndpointGroup =
| 'credentials' | 'credentials'
| 'workflows' | 'workflows'
| 'publicApi' | 'publicApi'
| 'nodes' | 'community-packages'
| 'ldap' | 'ldap'
| 'saml' | 'saml'
| 'sourceControl' | 'sourceControl'

View file

@ -25,7 +25,6 @@ import {
LdapController, LdapController,
MFAController, MFAController,
MeController, MeController,
NodesController,
OwnerController, OwnerController,
PasswordResetController, PasswordResetController,
TagsController, TagsController,
@ -34,12 +33,10 @@ import {
import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares'; import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { variablesController } from '@/environments/variables/variables.controller'; import { variablesController } from '@/environments/variables/variables.controller';
import { LdapManager } from '@/Ldap/LdapManager.ee'; import { LdapManager } from '@/Ldap/LdapManager.ee';
import { handleLdapInit } from '@/Ldap/helpers'; import { handleLdapInit } from '@/Ldap/helpers';
import { Push } from '@/push';
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
import { SamlController } from '@/sso/saml/routes/saml.controller.ee'; import { SamlController } from '@/sso/saml/routes/saml.controller.ee';
import { EventBusController } from '@/eventbus/eventBus.controller'; import { EventBusController } from '@/eventbus/eventBus.controller';
@ -242,17 +239,11 @@ export const setupTestServer = ({
case 'sourceControl': case 'sourceControl':
registerController(app, config, Container.get(SourceControlController)); registerController(app, config, Container.get(SourceControlController));
break; break;
case 'nodes': case 'community-packages':
registerController( const { CommunityPackagesController } = await import(
app, '@/controllers/communityPackages.controller'
config,
new NodesController(
config,
Container.get(LoadNodesAndCredentials),
Container.get(Push),
internalHooks,
),
); );
registerController(app, config, Container.get(CommunityPackagesController));
case 'me': case 'me':
registerController( registerController(
app, app,

View file

@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { mocked } from 'jest-mock'; import { mocked } from 'jest-mock';
import { Container } from 'typedi';
import type { ICredentialTypes, INode, INodesAndCredentials } from 'n8n-workflow'; import type { INode } from 'n8n-workflow';
import { LoggerProxy, NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow'; import { LoggerProxy, NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
@ -11,22 +12,19 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { Role } from '@db/entities/Role'; import { Role } from '@db/entities/Role';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import { randomEmail, randomName } from '../integration/shared/random';
import * as Helpers from './Helpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import { mock } from 'jest-mock-extended'; import { ExternalHooks } from '@/ExternalHooks';
import type { ExternalHooks } from '@/ExternalHooks';
import { Container } from 'typedi';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils/';
import { Push } from '@/push'; import { Push } from '@/push';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { NodeTypes } from '@/NodeTypes';
import { SecretsHelper } from '@/SecretsHelpers'; import { SecretsHelper } from '@/SecretsHelpers';
import { WebhookService } from '@/services/webhook.service'; import { WebhookService } from '@/services/webhook.service';
import { VariablesService } from '../../src/environments/variables/variables.service'; import { VariablesService } from '@/environments/variables/variables.service';
import { mockInstance } from '../integration/shared/utils/';
import { randomEmail, randomName } from '../integration/shared/random';
import * as Helpers from './Helpers';
/** /**
* TODO: * TODO:
@ -114,13 +112,6 @@ jest.mock('@/Db', () => {
return fakeQueryBuilder; return fakeQueryBuilder;
}), }),
}, },
Webhook: {
clear: jest.fn(),
delete: jest.fn(),
},
Variables: {
find: jest.fn(() => []),
},
}, },
}; };
}); });
@ -140,37 +131,24 @@ const workflowExecuteAdditionalDataExecuteErrorWorkflowSpy = jest.spyOn(
); );
describe('ActiveWorkflowRunner', () => { describe('ActiveWorkflowRunner', () => {
let externalHooks: ExternalHooks; mockInstance(ActiveExecutions);
let activeWorkflowRunner: ActiveWorkflowRunner; const externalHooks = mockInstance(ExternalHooks);
const webhookService = mockInstance(WebhookService); const webhookService = mockInstance(WebhookService);
mockInstance(Push);
mockInstance(SecretsHelper);
const variablesService = mockInstance(VariablesService);
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
Object.assign(nodesAndCredentials, {
loadedNodes: MOCK_NODE_TYPES_DATA,
known: { nodes: {}, credentials: {} },
types: { nodes: [], credentials: [] },
});
const activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
beforeAll(async () => { beforeAll(async () => {
LoggerProxy.init(getLogger()); LoggerProxy.init(getLogger());
const nodesAndCredentials: INodesAndCredentials = { variablesService.getAllCached.mockResolvedValue([]);
loaded: {
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
const mockVariablesService = {
getAllCached: jest.fn(() => []),
};
Container.set(LoadNodesAndCredentials, nodesAndCredentials);
Container.set(VariablesService, mockVariablesService);
mockInstance(Push);
mockInstance(SecretsHelper);
});
beforeEach(() => {
externalHooks = mock();
activeWorkflowRunner = new ActiveWorkflowRunner(
new ActiveExecutions(),
externalHooks,
Container.get(NodeTypes),
webhookService,
);
}); });
afterEach(async () => { afterEach(async () => {

View file

@ -1,13 +1,11 @@
import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils';
describe('CredentialTypes', () => { describe('CredentialTypes', () => {
const mockNodesAndCredentials: INodesAndCredentials = { const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loaded: { loadedCredentials: {
nodes: {},
credentials: {
fakeFirstCredential: { fakeFirstCredential: {
type: { type: {
name: 'fakeFirstCredential', name: 'fakeFirstCredential',
@ -25,12 +23,7 @@ describe('CredentialTypes', () => {
sourcePath: '', sourcePath: '',
}, },
}, },
}, });
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
const credentialTypes = Container.get(CredentialTypes); const credentialTypes = Container.get(CredentialTypes);
@ -39,7 +32,7 @@ describe('CredentialTypes', () => {
}); });
test('Should return correct credential type for valid name', () => { test('Should return correct credential type for valid name', () => {
const mockedCredentialTypes = mockNodesAndCredentials.loaded.credentials; const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual( expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
mockedCredentialTypes.fakeFirstCredential.type, mockedCredentialTypes.fakeFirstCredential.type,
); );

View file

@ -2,26 +2,22 @@ import type {
IAuthenticateGeneric, IAuthenticateGeneric,
ICredentialDataDecryptedObject, ICredentialDataDecryptedObject,
ICredentialType, ICredentialType,
ICredentialTypes,
IHttpRequestOptions, IHttpRequestOptions,
INode, INode,
INodeProperties, INodeProperties,
INodesAndCredentials,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { Workflow } from 'n8n-workflow'; import { Workflow } from 'n8n-workflow';
import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialsHelper } from '@/CredentialsHelper';
import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../integration/shared/utils';
describe('CredentialsHelper', () => { describe('CredentialsHelper', () => {
const TEST_ENCRYPTION_KEY = 'test'; const TEST_ENCRYPTION_KEY = 'test';
const mockNodesAndCredentials: INodesAndCredentials = { const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
loaded: { loadedNodes: {
nodes: {
'test.set': { 'test.set': {
sourcePath: '', sourcePath: '',
type: { type: {
@ -55,15 +51,9 @@ describe('CredentialsHelper', () => {
}, },
}, },
}, },
credentials: {}, });
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
};
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials); const nodeTypes = mockInstance(NodeTypes);
const nodeTypes = Container.get(NodeTypes);
describe('authenticate', () => { describe('authenticate', () => {
const tests: Array<{ const tests: Array<{
@ -280,20 +270,14 @@ describe('CredentialsHelper', () => {
for (const testData of tests) { for (const testData of tests) {
test(testData.description, async () => { test(testData.description, async () => {
mockNodesAndCredentials.loaded.credentials = { mockNodesAndCredentials.loadedCredentials = {
[testData.input.credentialType.name]: { [testData.input.credentialType.name]: {
type: testData.input.credentialType, type: testData.input.credentialType,
sourcePath: '', sourcePath: '',
}, },
}; };
const credentialTypes = Container.get(CredentialTypes); const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY);
const credentialsHelper = new CredentialsHelper(
TEST_ENCRYPTION_KEY,
credentialTypes,
nodeTypes,
);
const result = await credentialsHelper.authenticate( const result = await credentialsHelper.authenticate(
testData.input.credentials, testData.input.credentials,

View file

@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { ICredentialTypes, INodeTypes } from 'n8n-workflow'; import { mock } from 'jest-mock-extended';
import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import type { ILogger, INodeTypes } from 'n8n-workflow';
import { LoggerProxy, SubworkflowOperationError, Workflow } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -14,35 +15,26 @@ import { UserService } from '@/services/user.service';
import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
import { WorkflowsService } from '@/workflows/workflows.services'; import { WorkflowsService } from '@/workflows/workflows.services';
import { OwnershipService } from '@/services/ownership.service';
import { mockInstance } from '../integration/shared/utils/';
import { import {
randomCredentialPayload as randomCred, randomCredentialPayload as randomCred,
randomPositiveDigit, randomPositiveDigit,
} from '../integration/shared/random'; } from '../integration/shared/random';
import * as testDb from '../integration/shared/testDb'; import * as testDb from '../integration/shared/testDb';
import { mockNodeTypesData } from './Helpers';
import type { SaveCredentialFunction } from '../integration/shared/types'; import type { SaveCredentialFunction } from '../integration/shared/types';
import { mockInstance } from '../integration/shared/utils/'; import { mockNodeTypesData } from './Helpers';
import { OwnershipService } from '@/services/ownership.service';
import { LoggerProxy } from 'n8n-workflow'; LoggerProxy.init(mock<ILogger>());
import { getLogger } from '@/Logger';
LoggerProxy.init(getLogger());
let mockNodeTypes: INodeTypes; let mockNodeTypes: INodeTypes;
let credentialOwnerRole: Role; let credentialOwnerRole: Role;
let workflowOwnerRole: Role; let workflowOwnerRole: Role;
let saveCredential: SaveCredentialFunction; let saveCredential: SaveCredentialFunction;
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);
mockInstance(LoadNodesAndCredentials, { mockInstance(LoadNodesAndCredentials, {
loaded: { loadedNodes: mockNodeTypesData(['start', 'actionNetwork']),
nodes: MOCK_NODE_TYPES_DATA,
credentials: {},
},
known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes,
}); });
beforeAll(async () => { beforeAll(async () => {

View file

@ -1,7 +1,9 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
import axios from 'axios'; import axios from 'axios';
import { mocked } from 'jest-mock';
import Container from 'typedi';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { import {
NODE_PACKAGE_PREFIX, NODE_PACKAGE_PREFIX,
@ -9,22 +11,20 @@ import {
NPM_PACKAGE_STATUS_GOOD, NPM_PACKAGE_STATUS_GOOD,
RESPONSE_ERROR_MESSAGES, RESPONSE_ERROR_MESSAGES,
} from '@/constants'; } from '@/constants';
import { InstalledPackages } from '@db/entities/InstalledPackages';
import { randomName } from '../../integration/shared/random';
import config from '@/config'; import config from '@/config';
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils'; import { InstalledPackages } from '@db/entities/InstalledPackages';
import { mocked } from 'jest-mock';
import type { CommunityPackages } from '@/Interfaces'; import type { CommunityPackages } from '@/Interfaces';
import { CommunityPackageService } from '@/services/communityPackage.service'; import { CommunityPackagesService } from '@/services/communityPackages.service';
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories'; import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
import Container from 'typedi';
import { InstalledNodes } from '@/databases/entities/InstalledNodes'; import { InstalledNodes } from '@/databases/entities/InstalledNodes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { import {
COMMUNITY_NODE_VERSION, COMMUNITY_NODE_VERSION,
COMMUNITY_PACKAGE_VERSION, COMMUNITY_PACKAGE_VERSION,
} from '../../integration/shared/constants'; } from '../../integration/shared/constants';
import type { PublicInstalledPackage } from 'n8n-workflow'; import { randomName } from '../../integration/shared/random';
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
jest.mock('fs/promises'); jest.mock('fs/promises');
jest.mock('child_process'); jest.mock('child_process');
@ -38,10 +38,8 @@ const execMock = ((...args) => {
cb(null, 'Done', ''); cb(null, 'Done', '');
}) as typeof exec; }) as typeof exec;
describe('CommunityPackageService', () => { describe('CommunityPackagesService', () => {
const installedNodesRepository = mockInstance(InstalledNodesRepository); const installedNodesRepository = mockInstance(InstalledNodesRepository);
Container.set(InstalledNodesRepository, installedNodesRepository);
installedNodesRepository.create.mockImplementation(() => { installedNodesRepository.create.mockImplementation(() => {
const nodeName = randomName(); const nodeName = randomName();
@ -54,7 +52,6 @@ describe('CommunityPackageService', () => {
}); });
const installedPackageRepository = mockInstance(InstalledPackagesRepository); const installedPackageRepository = mockInstance(InstalledPackagesRepository);
installedPackageRepository.create.mockImplementation(() => { installedPackageRepository.create.mockImplementation(() => {
return Object.assign(new InstalledPackages(), { return Object.assign(new InstalledPackages(), {
packageName: mockPackageName(), packageName: mockPackageName(),
@ -62,7 +59,9 @@ describe('CommunityPackageService', () => {
}); });
}); });
const communityPackageService = new CommunityPackageService(installedPackageRepository); mockInstance(LoadNodesAndCredentials);
const communityPackagesService = Container.get(CommunityPackagesService);
beforeEach(() => { beforeEach(() => {
config.load(config.default); config.load(config.default);
@ -70,18 +69,18 @@ describe('CommunityPackageService', () => {
describe('parseNpmPackageName()', () => { describe('parseNpmPackageName()', () => {
test('should fail with empty package name', () => { test('should fail with empty package name', () => {
expect(() => communityPackageService.parseNpmPackageName('')).toThrowError(); expect(() => communityPackagesService.parseNpmPackageName('')).toThrowError();
}); });
test('should fail with invalid package prefix name', () => { test('should fail with invalid package prefix name', () => {
expect(() => expect(() =>
communityPackageService.parseNpmPackageName('INVALID_PREFIX@123'), communityPackagesService.parseNpmPackageName('INVALID_PREFIX@123'),
).toThrowError(); ).toThrowError();
}); });
test('should parse valid package name', () => { test('should parse valid package name', () => {
const name = mockPackageName(); const name = mockPackageName();
const parsed = communityPackageService.parseNpmPackageName(name); const parsed = communityPackagesService.parseNpmPackageName(name);
expect(parsed.rawString).toBe(name); expect(parsed.rawString).toBe(name);
expect(parsed.packageName).toBe(name); expect(parsed.packageName).toBe(name);
@ -93,7 +92,7 @@ describe('CommunityPackageService', () => {
const name = mockPackageName(); const name = mockPackageName();
const version = '0.1.1'; const version = '0.1.1';
const fullPackageName = `${name}@${version}`; const fullPackageName = `${name}@${version}`;
const parsed = communityPackageService.parseNpmPackageName(fullPackageName); const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
expect(parsed.rawString).toBe(fullPackageName); expect(parsed.rawString).toBe(fullPackageName);
expect(parsed.packageName).toBe(name); expect(parsed.packageName).toBe(name);
@ -106,7 +105,7 @@ describe('CommunityPackageService', () => {
const name = mockPackageName(); const name = mockPackageName();
const version = '0.1.1'; const version = '0.1.1';
const fullPackageName = `${scope}/${name}@${version}`; const fullPackageName = `${scope}/${name}@${version}`;
const parsed = communityPackageService.parseNpmPackageName(fullPackageName); const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
expect(parsed.rawString).toBe(fullPackageName); expect(parsed.rawString).toBe(fullPackageName);
expect(parsed.packageName).toBe(`${scope}/${name}`); expect(parsed.packageName).toBe(`${scope}/${name}`);
@ -134,7 +133,7 @@ describe('CommunityPackageService', () => {
mocked(exec).mockImplementation(execMock); mocked(exec).mockImplementation(execMock);
await communityPackageService.executeNpmCommand('ls'); await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled(); expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled(); expect(exec).toHaveBeenCalled();
@ -144,7 +143,7 @@ describe('CommunityPackageService', () => {
test('should make sure folder exists', async () => { test('should make sure folder exists', async () => {
mocked(exec).mockImplementation(execMock); mocked(exec).mockImplementation(execMock);
await communityPackageService.executeNpmCommand('ls'); await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled(); expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled(); expect(exec).toHaveBeenCalled();
expect(fsMkdir).toBeCalledTimes(0); expect(fsMkdir).toBeCalledTimes(0);
@ -156,7 +155,7 @@ describe('CommunityPackageService', () => {
throw new Error('Folder does not exist.'); throw new Error('Folder does not exist.');
}); });
await communityPackageService.executeNpmCommand('ls'); await communityPackagesService.executeNpmCommand('ls');
expect(fsAccess).toHaveBeenCalled(); expect(fsAccess).toHaveBeenCalled();
expect(exec).toHaveBeenCalled(); expect(exec).toHaveBeenCalled();
@ -172,7 +171,7 @@ describe('CommunityPackageService', () => {
mocked(exec).mockImplementation(erroringExecMock); mocked(exec).mockImplementation(erroringExecMock);
const call = async () => communityPackageService.executeNpmCommand('ls'); const call = async () => communityPackagesService.executeNpmCommand('ls');
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND); await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
@ -186,7 +185,7 @@ describe('CommunityPackageService', () => {
test('should return same list if availableUpdates is undefined', () => { test('should return same list if availableUpdates is undefined', () => {
const fakePkgs = mockPackagePair(); const fakePkgs = mockPackagePair();
const crossedPkgs = communityPackageService.matchPackagesWithUpdates(fakePkgs); const crossedPkgs = communityPackagesService.matchPackagesWithUpdates(fakePkgs);
expect(crossedPkgs).toEqual(fakePkgs); expect(crossedPkgs).toEqual(fakePkgs);
}); });
@ -210,7 +209,7 @@ describe('CommunityPackageService', () => {
}; };
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates); communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
expect(crossedPkgA.updateAvailable).toBe('0.2.0'); expect(crossedPkgA.updateAvailable).toBe('0.2.0');
expect(crossedPkgB.updateAvailable).toBe('0.3.0'); expect(crossedPkgB.updateAvailable).toBe('0.3.0');
@ -229,7 +228,7 @@ describe('CommunityPackageService', () => {
}; };
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates); communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
expect(crossedPkgA.updateAvailable).toBeUndefined(); expect(crossedPkgA.updateAvailable).toBeUndefined();
expect(crossedPkgB.updateAvailable).toBe('0.3.0'); expect(crossedPkgB.updateAvailable).toBe('0.3.0');
@ -239,12 +238,12 @@ describe('CommunityPackageService', () => {
describe('matchMissingPackages()', () => { describe('matchMissingPackages()', () => {
test('should not match failed packages that do not exist', () => { test('should not match failed packages that do not exist', () => {
const fakePkgs = mockPackagePair(); const fakePkgs = mockPackagePair();
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`; setMissingPackages([
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
`${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`,
]);
const matchedPackages = communityPackageService.matchMissingPackages( const matchedPackages = communityPackagesService.matchMissingPackages(fakePkgs);
fakePkgs,
notFoundPkgNames,
);
expect(matchedPackages).toEqual(fakePkgs); expect(matchedPackages).toEqual(fakePkgs);
@ -256,12 +255,15 @@ describe('CommunityPackageService', () => {
test('should match failed packages that should be present', () => { test('should match failed packages that should be present', () => {
const [pkgA, pkgB] = mockPackagePair(); const [pkgA, pkgB] = mockPackagePair();
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@${pkgA.installedVersion}`; setMissingPackages([
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
`${pkgA.packageName}@${pkgA.installedVersion}`,
]);
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages( const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([
[pkgA, pkgB], pkgA,
notFoundPkgNames, pkgB,
); ]);
expect(matchedPkgA.failedLoading).toBe(true); expect(matchedPkgA.failedLoading).toBe(true);
expect(matchedPkgB.failedLoading).toBeUndefined(); expect(matchedPkgB.failedLoading).toBeUndefined();
@ -269,11 +271,14 @@ describe('CommunityPackageService', () => {
test('should match failed packages even if version is wrong', () => { test('should match failed packages even if version is wrong', () => {
const [pkgA, pkgB] = mockPackagePair(); const [pkgA, pkgB] = mockPackagePair();
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@123.456.789`; setMissingPackages([
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages( `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
[pkgA, pkgB], `${pkgA.packageName}@123.456.789`,
notFoundPackageList, ]);
); const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([
pkgA,
pkgB,
]);
expect(matchedPkgA.failedLoading).toBe(true); expect(matchedPkgA.failedLoading).toBe(true);
expect(matchedPkgB.failedLoading).toBeUndefined(); expect(matchedPkgB.failedLoading).toBeUndefined();
@ -282,7 +287,7 @@ describe('CommunityPackageService', () => {
describe('checkNpmPackageStatus()', () => { describe('checkNpmPackageStatus()', () => {
test('should call axios.post', async () => { test('should call axios.post', async () => {
await communityPackageService.checkNpmPackageStatus(mockPackageName()); await communityPackagesService.checkNpmPackageStatus(mockPackageName());
expect(axios.post).toHaveBeenCalled(); expect(axios.post).toHaveBeenCalled();
}); });
@ -292,7 +297,7 @@ describe('CommunityPackageService', () => {
throw new Error('Something went wrong'); throw new Error('Something went wrong');
}); });
const result = await communityPackageService.checkNpmPackageStatus(mockPackageName()); const result = await communityPackagesService.checkNpmPackageStatus(mockPackageName());
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD); expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
}); });
@ -300,7 +305,7 @@ describe('CommunityPackageService', () => {
test('should warn if package is banned', async () => { test('should warn if package is banned', async () => {
mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } }); mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } });
const result = (await communityPackageService.checkNpmPackageStatus( const result = (await communityPackagesService.checkNpmPackageStatus(
mockPackageName(), mockPackageName(),
)) as CommunityPackages.PackageStatusCheck; )) as CommunityPackages.PackageStatusCheck;
@ -311,47 +316,50 @@ describe('CommunityPackageService', () => {
describe('hasPackageLoadedSuccessfully()', () => { describe('hasPackageLoadedSuccessfully()', () => {
test('should return true when failed package list does not exist', () => { test('should return true when failed package list does not exist', () => {
config.set<string>('nodes.packagesMissing', undefined); setMissingPackages([]);
expect(communityPackagesService.hasPackageLoaded('package')).toBe(true);
expect(communityPackageService.hasPackageLoaded('package')).toBe(true);
}); });
test('should return true when package is not in the list of missing packages', () => { test('should return true when package is not in the list of missing packages', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0'); setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']);
expect(communityPackagesService.hasPackageLoaded('packageC')).toBe(true);
expect(communityPackageService.hasPackageLoaded('packageC')).toBe(true);
}); });
test('should return false when package is in the list of missing packages', () => { test('should return false when package is in the list of missing packages', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0'); setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']);
expect(communityPackagesService.hasPackageLoaded('packageA')).toBe(false);
expect(communityPackageService.hasPackageLoaded('packageA')).toBe(false);
}); });
}); });
describe('removePackageFromMissingList()', () => { describe('removePackageFromMissingList()', () => {
test('should do nothing if key does not exist', () => { test('should do nothing if key does not exist', () => {
config.set<string>('nodes.packagesMissing', undefined); setMissingPackages([]);
communityPackagesService.removePackageFromMissingList('packageA');
communityPackageService.removePackageFromMissingList('packageA'); expect(communityPackagesService.missingPackages).toBeEmptyArray();
expect(config.get('nodes.packagesMissing')).toBeUndefined();
}); });
test('should remove only correct package from list', () => { test('should remove only correct package from list', () => {
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageC@0.2.0'); setMissingPackages(['packageA@0.1.0', 'packageB@0.2.0', 'packageC@0.2.0']);
communityPackageService.removePackageFromMissingList('packageB'); communityPackagesService.removePackageFromMissingList('packageB');
expect(config.get('nodes.packagesMissing')).toBe('packageA@0.1.0 packageC@0.2.0'); expect(communityPackagesService.missingPackages).toEqual([
'packageA@0.1.0',
'packageC@0.2.0',
]);
}); });
test('should not remove if package is not in the list', () => { test('should not remove if package is not in the list', () => {
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageB@0.2.0'; const failedToLoadList = ['packageA@0.1.0', 'packageB@0.2.0', 'packageB@0.2.0'];
config.set('nodes.packagesMissing', failedToLoadList); setMissingPackages(failedToLoadList);
communityPackageService.removePackageFromMissingList('packageC'); communityPackagesService.removePackageFromMissingList('packageC');
expect(config.get('nodes.packagesMissing')).toBe(failedToLoadList); expect(communityPackagesService.missingPackages).toEqual(failedToLoadList);
}); });
}); });
const setMissingPackages = (missingPackages: string[]) => {
Object.assign(communityPackagesService, { missingPackages });
};
}); });

View file

@ -5,7 +5,7 @@ import { get, post, makeRestApiRequest } from '@/utils';
export async function getInstalledCommunityNodes( export async function getInstalledCommunityNodes(
context: IRestApiContext, context: IRestApiContext,
): Promise<PublicInstalledPackage[]> { ): Promise<PublicInstalledPackage[]> {
const response = await get(context.baseUrl, '/nodes'); const response = await get(context.baseUrl, '/community-packages');
return response.data || []; return response.data || [];
} }
@ -13,16 +13,16 @@ export async function installNewPackage(
context: IRestApiContext, context: IRestApiContext,
name: string, name: string,
): Promise<PublicInstalledPackage> { ): Promise<PublicInstalledPackage> {
return post(context.baseUrl, '/nodes', { name }); return post(context.baseUrl, '/community-packages', { name });
} }
export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> { export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> {
return makeRestApiRequest(context, 'DELETE', '/nodes', { name }); return makeRestApiRequest(context, 'DELETE', '/community-packages', { name });
} }
export async function updatePackage( export async function updatePackage(
context: IRestApiContext, context: IRestApiContext,
name: string, name: string,
): Promise<PublicInstalledPackage> { ): Promise<PublicInstalledPackage> {
return makeRestApiRequest(context, 'PATCH', '/nodes', { name }); return makeRestApiRequest(context, 'PATCH', '/community-packages', { name });
} }

View file

@ -1712,17 +1712,6 @@ type LoadedData<T> = Record<string, LoadedClass<T>>;
export type ICredentialTypeData = LoadedData<ICredentialType>; export type ICredentialTypeData = LoadedData<ICredentialType>;
export type INodeTypeData = LoadedData<INodeType | IVersionedNodeType>; export type INodeTypeData = LoadedData<INodeType | IVersionedNodeType>;
export type LoadedNodesAndCredentials = {
nodes: INodeTypeData;
credentials: ICredentialTypeData;
};
export interface INodesAndCredentials {
known: KnownNodesAndCredentials;
loaded: LoadedNodesAndCredentials;
credentialTypes: ICredentialTypes;
}
export interface IRun { export interface IRun {
data: IRunExecutionData; data: IRunExecutionData;
finished?: boolean; finished?: boolean;