From c5ee06cc61e7ed040fd2498d5d7e4e8d2067e164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 9 Oct 2023 16:09:23 +0200 Subject: [PATCH] refactor(core): Refactor nodes loading (no-changelog) (#7283) fixes PAY-605 --- .dockerignore | 1 - packages/cli/package.json | 3 +- packages/cli/src/CredentialTypes.ts | 28 +-- packages/cli/src/CredentialsHelper.ts | 18 +- packages/cli/src/CredentialsOverwrites.ts | 28 +-- packages/cli/src/LoadNodesAndCredentials.ts | 230 +++++++----------- packages/cli/src/NodeTypes.ts | 22 +- packages/cli/src/ReloadNodesAndCredentials.ts | 47 ---- packages/cli/src/Server.ts | 51 ++-- packages/cli/src/WorkflowRunnerProcess.ts | 4 - packages/cli/src/audit/risks/nodes.risk.ts | 10 +- packages/cli/src/commands/BaseCommand.ts | 10 +- packages/cli/src/commands/start.ts | 14 +- packages/cli/src/commands/worker.ts | 2 +- packages/cli/src/config/schema.ts | 6 - ...ler.ts => communityPackages.controller.ts} | 61 ++--- packages/cli/src/controllers/index.ts | 1 - ...ervice.ts => communityPackages.service.ts} | 146 +++++++---- packages/cli/src/services/frontend.service.ts | 67 +++++ .../test/integration/audit/nodes.risk.test.ts | 12 +- .../integration/commands/import.cmd.test.ts | 16 +- ...test.ts => community-packages.api.test.ts} | 116 ++++----- packages/cli/test/integration/shared/types.ts | 2 +- .../integration/shared/utils/testServer.ts | 17 +- .../test/unit/ActiveWorkflowRunner.test.ts | 66 ++--- .../cli/test/unit/CredentialTypes.test.ts | 43 ++-- .../cli/test/unit/CredentialsHelper.test.ts | 86 +++---- .../cli/test/unit/PermissionChecker.test.ts | 24 +- ...t.ts => communityPackages.service.test.ts} | 136 ++++++----- packages/editor-ui/src/api/communityNodes.ts | 8 +- packages/workflow/src/Interfaces.ts | 11 - 31 files changed, 603 insertions(+), 683 deletions(-) delete mode 100644 packages/cli/src/ReloadNodesAndCredentials.ts rename packages/cli/src/controllers/{nodes.controller.ts => communityPackages.controller.ts} (78%) rename packages/cli/src/services/{communityPackage.service.ts => communityPackages.service.ts} (68%) create mode 100644 packages/cli/src/services/frontend.service.ts rename packages/cli/test/integration/{nodes.api.test.ts => community-packages.api.test.ts} (51%) rename packages/cli/test/unit/services/{communityPackage.service.test.ts => communityPackages.service.test.ts} (71%) diff --git a/.dockerignore b/.dockerignore index b907ce51f2..dfec9df290 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,3 @@ packages/**/.turbo .github *.tsbuildinfo packages/cli/dist/**/e2e.* -packages/cli/dist/ReloadNodesAndCredentials.* diff --git a/packages/cli/package.json b/packages/cli/package.json index a904bb1a87..ff10b3013f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -61,8 +61,7 @@ "templates", "dist", "oclif.manifest.json", - "!dist/**/e2e.*", - "!dist/ReloadNodesAndCredentials.*" + "!dist/**/e2e.*" ], "devDependencies": { "@apidevtools/swagger-cli": "4.0.0", diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index 6ea919cea4..5e4277f1d0 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -1,17 +1,16 @@ +import { Service } from 'typedi'; import { loadClassInIsolation } from 'n8n-core'; import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow'; -import { Service } from 'typedi'; -import { RESPONSE_ERROR_MESSAGES } from './constants'; -import { LoadNodesAndCredentials } from './LoadNodesAndCredentials'; +import { RESPONSE_ERROR_MESSAGES } from '@/constants'; +import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; @Service() export class CredentialTypes implements ICredentialTypes { - constructor(private nodesAndCredentials: LoadNodesAndCredentials) { - nodesAndCredentials.credentialTypes = this; - } + constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) {} 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 { @@ -19,14 +18,14 @@ export class CredentialTypes implements ICredentialTypes { } getNodeTypesToTestWith(type: string): string[] { - return this.knownCredentials[type]?.nodesToTestWith ?? []; + return this.loadNodesAndCredentials.knownCredentials[type]?.nodesToTestWith ?? []; } /** * Returns all parent types of the given credential type */ getParentTypes(typeName: string): string[] { - const extendsArr = this.knownCredentials[typeName]?.extends ?? []; + const extendsArr = this.loadNodesAndCredentials.knownCredentials[typeName]?.extends ?? []; if (extendsArr.length) { extendsArr.forEach((type) => { extendsArr.push(...this.getParentTypes(type)); @@ -36,12 +35,11 @@ export class CredentialTypes implements ICredentialTypes { } private getCredential(type: string): LoadedClass { - const loadedCredentials = this.loadedCredentials; + const { loadedCredentials, knownCredentials } = this.loadNodesAndCredentials; if (type in loadedCredentials) { return loadedCredentials[type]; } - const knownCredentials = this.knownCredentials; if (type in knownCredentials) { const { className, sourcePath } = knownCredentials[type]; const loaded: ICredentialType = loadClassInIsolation(sourcePath, className); @@ -50,12 +48,4 @@ export class CredentialTypes implements ICredentialTypes { } 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; - } } diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index f8bfc0cc8a..f8e08e7210 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ @@ -89,13 +88,11 @@ const mockNodeTypes: INodeTypes = { }; export class CredentialsHelper extends ICredentialsHelper { - constructor( - encryptionKey: string, - private credentialTypes = Container.get(CredentialTypes), - private nodeTypes = Container.get(NodeTypes), - ) { - super(encryptionKey); - } + private credentialTypes = Container.get(CredentialTypes); + + private nodeTypes = Container.get(NodeTypes); + + private credentialsOverwrites = Container.get(CredentialsOverwrites); /** * Add the required authentication information to the request @@ -388,7 +385,10 @@ export class CredentialsHelper extends ICredentialsHelper { const credentialsProperties = this.getCredentialsProperties(type); // 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 let decryptedData = NodeHelpers.getNodeParameters( diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts index 81b86c3651..e8df84f599 100644 --- a/packages/cli/src/CredentialsOverwrites.ts +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -1,14 +1,17 @@ -import config from '@/config'; +import { Service } from 'typedi'; 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 { CredentialTypes } from '@/CredentialTypes'; -class CredentialsOverwritesClass { +@Service() +export class CredentialsOverwrites { private overwriteData: ICredentialsOverwrite = {}; private resolvedTypes: string[] = []; - constructor(private credentialTypes: ICredentialTypes) { + constructor(private credentialTypes: CredentialTypes) { const data = config.getEnv('credentials.overwrite.data'); const overwriteData = jsonParse(data, { errorMessage: 'The credentials-overwrite is not valid JSON.', @@ -96,20 +99,3 @@ class CredentialsOverwritesClass { 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; -} diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 74f717442e..9c1ddd900b 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -1,5 +1,8 @@ -import uniq from 'lodash/uniq'; 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 { CUSTOM_EXTENSION_ENV, @@ -9,36 +12,30 @@ import { LazyPackageDirectoryLoader, } from 'n8n-core'; import type { - ICredentialTypes, - ILogger, - INodesAndCredentials, KnownNodesAndCredentials, INodeTypeDescription, - LoadedNodesAndCredentials, + INodeTypeData, + ICredentialTypeData, } 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 type { InstalledPackages } from '@db/entities/InstalledPackages'; -import { CommunityPackageService } from './services/communityPackage.service'; import { - GENERATED_STATIC_DIR, - RESPONSE_ERROR_MESSAGES, CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_NAME, inTest, CLI_DIR, inE2ETests, } from '@/constants'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; -import Container, { Service } from 'typedi'; + +interface LoadedNodesAndCredentials { + nodes: INodeTypeData; + credentials: ICredentialTypeData; +} @Service() -export class LoadNodesAndCredentials implements INodesAndCredentials { - known: KnownNodesAndCredentials = { nodes: {}, credentials: {} }; +export class LoadNodesAndCredentials { + private known: KnownNodesAndCredentials = { nodes: {}, credentials: {} }; loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} }; @@ -50,20 +47,20 @@ export class LoadNodesAndCredentials implements INodesAndCredentials { includeNodes = config.getEnv('nodes.include'); - credentialTypes: ICredentialTypes; - - logger: ILogger; - private downloadFolder: string; + private postProcessors: Array<() => Promise> = []; + async init() { + if (inTest) throw new Error('Not available in tests'); + // Make sure the imported modules can resolve dependencies fine. const delimiter = process.platform === 'win32' ? ';' : ':'; process.env.NODE_PATH = module.paths.join(delimiter); // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unsafe-call - if (!inTest) module.constructor._initPaths(); + module.constructor._initPaths(); if (!inE2ETests) { this.excludeNodes = this.excludeNodes ?? []; @@ -91,48 +88,30 @@ export class LoadNodesAndCredentials implements INodesAndCredentials { await this.loadNodesFromCustomDirectories(); await this.postProcessLoaders(); - this.injectCustomApiCallOptions(); } - async generateTypesForFrontend() { - const credentialsOverwrites = CredentialsOverwrites().getAll(); - 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)); - }); + addPostProcessor(fn: () => Promise) { + this.postProcessors.push(fn); + } - if (credential.name in credentialsOverwrites) { - overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name])); - } + isKnownNode(type: string) { + return type in this.known.nodes; + } - if (overwrittenProperties.length) { - credential.__overwrittenProperties = uniq(overwrittenProperties); - } - } + get loadedCredentials() { + return this.loaded.credentials; + } - // pre-render all the node and credential types as static json files - await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true }); + get loadedNodes() { + return this.loaded.nodes; + } - const writeStaticJSON = async (name: string, data: object[]) => { - 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(); - }; + get knownCredentials() { + return this.known.credentials; + } - await writeStaticJSON('nodes', this.types.nodes); - await writeStaticJSON('credentials', this.types.credentials); + get knownNodes() { + return this.known.nodes; } 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[] { const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()]; @@ -180,93 +171,16 @@ export class LoadNodesAndCredentials implements INodesAndCredentials { } } - 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}` : ''}`; - - 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; - } - + async loadPackage(packageName: string) { const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName); - - 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) { - // 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); - } + return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath); } - async installNpmModule(packageName: string, version?: string): Promise { - return this.installOrUpdateNpmModule(packageName, { version }); - } - - async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise { - const communityPackageService = Container.get(CommunityPackageService); - - await communityPackageService.executeNpmCommand(`npm remove ${packageName}`); - - await communityPackageService.removePackageFromDatabase(installedPackage); - + async unloadPackage(packageName: string) { if (packageName in this.loaders) { this.loaders[packageName].reset(); delete this.loaders[packageName]; } - - await this.postProcessLoaders(); - await this.generateTypesForFrontend(); - } - - async updateNpmModule( - packageName: string, - installedPackage: InstalledPackages, - ): Promise { - 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); + }); } } diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 0f1d6ebddc..7831f3bd2c 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -16,12 +16,8 @@ import type { Dirent } from 'fs'; @Service() export class NodeTypes implements INodeTypes { - constructor(private nodesAndCredentials: LoadNodesAndCredentials) {} - - init() { - // Some nodeTypes need to get special parameters applied like the - // polling nodes the polling times - this.applySpecialNodeParameters(); + constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) { + loadNodesAndCredentials.addPostProcessor(async () => this.applySpecialNodeParameters()); } /** @@ -50,20 +46,20 @@ export class NodeTypes implements INodeTypes { return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version); } + /* Some nodeTypes need to get special parameters applied like the polling nodes the polling times */ applySpecialNodeParameters() { - for (const nodeTypeData of Object.values(this.loadedNodes)) { + for (const nodeTypeData of Object.values(this.loadNodesAndCredentials.loadedNodes)) { const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type); NodeHelpers.applySpecialNodeParameters(nodeType); } } private getNode(type: string): LoadedClass { - const loadedNodes = this.loadedNodes; + const { loadedNodes, knownNodes } = this.loadNodesAndCredentials; if (type in loadedNodes) { return loadedNodes[type]; } - const knownNodes = this.knownNodes; if (type in knownNodes) { const { className, sourcePath } = knownNodes[type]; const loaded: INodeType = loadClassInIsolation(sourcePath, className); @@ -74,14 +70,6 @@ export class NodeTypes implements INodeTypes { 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({ nodeSourcePath, longNodeType, diff --git a/packages/cli/src/ReloadNodesAndCredentials.ts b/packages/cli/src/ReloadNodesAndCredentials.ts deleted file mode 100644 index 3867cc0ed6..0000000000 --- a/packages/cli/src/ReloadNodesAndCredentials.ts +++ /dev/null @@ -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); - }); -}; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5cdd87cf6..19960fea37 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -11,7 +11,7 @@ import assert from 'assert'; import { exec as callbackExec } from 'child_process'; import { access as fsAccess } from 'fs/promises'; 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 { promisify } from 'util'; import cookieParser from 'cookie-parser'; @@ -86,7 +86,6 @@ import { LdapController, MeController, MFAController, - NodesController, NodeTypesController, OwnerController, PasswordResetController, @@ -172,6 +171,7 @@ import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; import { TOTPService } from './Mfa/totp.service'; import { MfaService } from './Mfa/mfa.service'; import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers'; +import type { FrontendService } from './services/frontend.service'; import { JwtService } from './services/jwt.service'; import { RoleService } from './services/role.service'; import { UserService } from './services/user.service'; @@ -202,6 +202,8 @@ export class Server extends AbstractServer { credentialTypes: ICredentialTypes; + frontendService: FrontendService; + postHog: PostHogClient; push: Push; @@ -362,6 +364,16 @@ export class Server extends AbstractServer { this.credentialTypes = Container.get(CredentialTypes); 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.waitTracker = Container.get(WaitTracker); this.postHog = Container.get(PostHogClient); @@ -419,8 +431,7 @@ export class Server extends AbstractServer { }; if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') { - const { reloadNodesAndCredentials } = await import('@/ReloadNodesAndCredentials'); - await reloadNodesAndCredentials(this.loadNodesAndCredentials, this.nodeTypes, this.push); + void this.loadNodesAndCredentials.setupHotReload(); } void Db.collections.Workflow.findOne({ @@ -435,7 +446,7 @@ export class Server extends AbstractServer { /** * Returns the current settings for the frontend */ - getSettingsForFrontend(): IN8nUISettings { + private async getSettingsForFrontend(): Promise { // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` const instanceBaseUrl = getInstanceBaseUrl(); this.frontendSettings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); @@ -506,8 +517,11 @@ export class Server extends AbstractServer { }); } - if (config.get('nodes.packagesMissing').length > 0) { - this.frontendSettings.missingPackages = true; + if (config.getEnv('nodes.communityPackages.enabled')) { + // 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(); @@ -585,9 +599,11 @@ export class Server extends AbstractServer { } if (config.getEnv('nodes.communityPackages.enabled')) { - controllers.push( - new NodesController(config, this.loadNodesAndCredentials, this.push, internalHooks), + // eslint-disable-next-line @typescript-eslint/naming-convention + const { CommunityPackagesController } = await import( + '@/controllers/communityPackages.controller' ); + controllers.push(Container.get(CommunityPackagesController)); } if (inE2ETests) { @@ -1480,9 +1496,9 @@ export class Server extends AbstractServer { return; } - CredentialsOverwrites().setData(body); + Container.get(CredentialsOverwrites).setData(body); - await this.loadNodesAndCredentials.generateTypesForFrontend(); + await this.frontendService?.generateTypes(); this.presetCredentialsLoaded = true; @@ -1509,22 +1525,13 @@ export class Server extends AbstractServer { const serveIcons: express.RequestHandler = async (req, res) => { let { scope, packageName } = req.params; if (scope) packageName = `@${scope}/${packageName}`; - const loader = this.loadNodesAndCredentials.loaders[packageName]; - if (loader) { - 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(); - } + const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl); + if (filePath) { try { await fsAccess(filePath); return res.sendFile(filePath); } catch {} } - res.sendStatus(404); }; diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index a69e22bda6..dee14511f8 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -35,8 +35,6 @@ import { WorkflowHooks, WorkflowOperationError, } from 'n8n-workflow'; -import { CredentialTypes } from '@/CredentialTypes'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import * as Db from '@/Db'; import { ExternalHooks } from '@/ExternalHooks'; import type { @@ -115,8 +113,6 @@ class WorkflowRunnerProcess { await loadNodesAndCredentials.init(); const nodeTypes = Container.get(NodeTypes); - const credentialTypes = Container.get(CredentialTypes); - CredentialsOverwrites(credentialTypes); // Load all external hooks const externalHooks = Container.get(ExternalHooks); diff --git a/packages/cli/src/audit/risks/nodes.risk.ts b/packages/cli/src/audit/risks/nodes.risk.ts index 4ac7796aa2..b1ef484b51 100644 --- a/packages/cli/src/audit/risks/nodes.risk.ts +++ b/packages/cli/src/audit/risks/nodes.risk.ts @@ -1,8 +1,9 @@ import * as path from 'path'; import glob from 'fast-glob'; +import { Container } from 'typedi'; +import config from '@/config'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { getNodeTypes } from '@/audit/utils'; -import { CommunityPackageService } from '@/services/communityPackage.service'; import { OFFICIAL_RISKY_NODE_TYPES, ENV_VARS_DOCS_URL, @@ -12,10 +13,13 @@ import { } from '@/audit/constants'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { Risk } from '@/audit/types'; -import { Container } from 'typedi'; 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((acc, pkg) => { pkg.installedNodes.forEach((node) => diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index a1628adf10..8397034df5 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -10,8 +10,6 @@ import config from '@/config'; import * as Db from '@/Db'; import * as CrashJournal from '@/CrashJournal'; import { LICENSE_FEATURES, inTest } from '@/constants'; -import { CredentialTypes } from '@/CredentialTypes'; -import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { initErrorHandling } from '@/ErrorReporting'; import { ExternalHooks } from '@/ExternalHooks'; import { NodeTypes } from '@/NodeTypes'; @@ -30,8 +28,6 @@ export abstract class BaseCommand extends Command { protected externalHooks: IExternalHooksClass; - protected loadNodesAndCredentials: LoadNodesAndCredentials; - protected nodeTypes: NodeTypes; protected userSettings: IUserSettings; @@ -54,12 +50,8 @@ export abstract class BaseCommand extends Command { // Make sure the settings exist this.userSettings = await UserSettings.prepareUserSettings(); - this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials); - await this.loadNodesAndCredentials.init(); + await Container.get(LoadNodesAndCredentials).init(); this.nodeTypes = Container.get(NodeTypes); - this.nodeTypes.init(); - const credentialTypes = Container.get(CredentialTypes); - CredentialsOverwrites(credentialTypes); await Db.init().catch(async (error: Error) => this.exitWithCrash('There was an error initializing DB', error), diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 98326e81db..b6d01aa0e1 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -23,7 +23,6 @@ import * as Db from '@/Db'; import * as GenericHelpers from '@/GenericHelpers'; import { Server } from '@/Server'; import { TestWebhooks } from '@/TestWebhooks'; -import { CommunityPackageService } from '@/services/communityPackage.service'; import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants'; import { eventBus } from '@/eventbus'; import { BaseCommand } from './BaseCommand'; @@ -257,8 +256,6 @@ export class Start extends BaseCommand { config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex')); } - await this.loadNodesAndCredentials.generateTypesForFrontend(); - await UserSettings.getEncryptionKey(); // 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'); if (areCommunityPackagesEnabled) { - await Container.get(CommunityPackageService).setMissingPackages( - this.loadNodesAndCredentials, - { - reinstallMissingPackages: flags.reinstallMissingPackages, - }, - ); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { CommunityPackagesService } = await import('@/services/communityPackages.service'); + await Container.get(CommunityPackagesService).setMissingPackages({ + reinstallMissingPackages: flags.reinstallMissingPackages, + }); } const dbType = config.getEnv('database.type'); diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index e26829d8bc..9fc960d02f 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -463,7 +463,7 @@ export class Worker extends BaseCommand { return; } - CredentialsOverwrites().setData(body); + Container.get(CredentialsOverwrites).setData(body); presetCredentialsLoaded = true; ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); } else { diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index ae92095441..7af99908f1 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -818,12 +818,6 @@ export const schema = { 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: { diff --git a/packages/cli/src/controllers/nodes.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts similarity index 78% rename from packages/cli/src/controllers/nodes.controller.ts rename to packages/cli/src/controllers/communityPackages.controller.ts index 159a55f08d..c31ec8ff82 100644 --- a/packages/cli/src/controllers/nodes.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -1,4 +1,6 @@ +import { Service } from 'typedi'; import { Request, Response, NextFunction } from 'express'; +import config from '@/config'; import { RESPONSE_ERROR_MESSAGES, STARTER_TEMPLATE_NAME, @@ -9,12 +11,9 @@ import { NodeRequest } from '@/requests'; import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { CommunityPackages } from '@/Interfaces'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { InternalHooks } from '@/InternalHooks'; import { Push } from '@/push'; -import { Config } from '@/config'; -import { CommunityPackageService } from '@/services/communityPackage.service'; -import Container from 'typedi'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; const { 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; } +@Service() @Authorized(['global', 'owner']) -@RestController('/nodes') -export class NodesController { - private communityPackageService: CommunityPackageService; - +@RestController('/community-packages') +export class CommunityPackagesController { constructor( - private config: Config, - private loadNodesAndCredentials: LoadNodesAndCredentials, private push: Push, private internalHooks: InternalHooks, - ) { - this.communityPackageService = Container.get(CommunityPackageService); - } + private communityPackagesService: CommunityPackagesService, + ) {} // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` @Middleware() 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({ status: 'error', message: 'Package management is disabled when running in "queue" mode', @@ -69,7 +64,7 @@ export class NodesController { let parsed: CommunityPackages.ParsedPackageName; try { - parsed = this.communityPackageService.parseNpmPackageName(name); + parsed = this.communityPackagesService.parseNpmPackageName(name); } catch (error) { throw new BadRequestError( 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 hasLoaded = this.communityPackageService.hasPackageLoaded(name); + const isInstalled = await this.communityPackagesService.isPackageInstalled(parsed.packageName); + const hasLoaded = this.communityPackagesService.hasPackageLoaded(name); if (isInstalled && hasLoaded) { 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') { throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`); @@ -105,7 +100,7 @@ export class NodesController { let installedPackage: InstalledPackages; try { - installedPackage = await this.loadNodesAndCredentials.installNpmModule( + installedPackage = await this.communityPackagesService.installNpmModule( parsed.packageName, parsed.version, ); @@ -130,7 +125,7 @@ export class NodesController { 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 installedPackage.installedNodes.forEach((node) => { @@ -156,7 +151,7 @@ export class NodesController { @Get('/') async getInstalledPackages() { - const installedPackages = await this.communityPackageService.getAllInstalledPackages(); + const installedPackages = await this.communityPackagesService.getAllInstalledPackages(); if (installedPackages.length === 0) return []; @@ -164,7 +159,7 @@ export class NodesController { try { const command = ['npm', 'outdated', '--json'].join(' '); - await this.communityPackageService.executeNpmCommand(command, { doNotHandleError: true }); + await this.communityPackagesService.executeNpmCommand(command, { doNotHandleError: true }); } catch (error) { // when there are updates, npm exits with code 1 // 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, pendingUpdates, ); try { - const missingPackages = this.config.get('nodes.packagesMissing') as string | undefined; - if (missingPackages) { - hydratedPackages = this.communityPackageService.matchMissingPackages( - hydratedPackages, - missingPackages, - ); + if (this.communityPackagesService.hasMissingPackages) { + hydratedPackages = this.communityPackagesService.matchMissingPackages(hydratedPackages); } } catch {} @@ -201,21 +192,21 @@ export class NodesController { } try { - this.communityPackageService.parseNpmPackageName(name); // sanitize input + this.communityPackagesService.parseNpmPackageName(name); // sanitize input } catch (error) { const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON; throw new BadRequestError(message); } - const installedPackage = await this.communityPackageService.findInstalledPackage(name); + const installedPackage = await this.communityPackagesService.findInstalledPackage(name); if (!installedPackage) { throw new BadRequestError(PACKAGE_NOT_INSTALLED); } try { - await this.loadNodesAndCredentials.removeNpmModule(name, installedPackage); + await this.communityPackagesService.removeNpmModule(name, installedPackage); } catch (error) { const message = [ `Error removing package "${name}"`, @@ -252,15 +243,15 @@ export class NodesController { } const previouslyInstalledPackage = - await this.communityPackageService.findInstalledPackage(name); + await this.communityPackagesService.findInstalledPackage(name); if (!previouslyInstalledPackage) { throw new BadRequestError(PACKAGE_NOT_INSTALLED); } try { - const newInstalledPackage = await this.loadNodesAndCredentials.updateNpmModule( - this.communityPackageService.parseNpmPackageName(name).packageName, + const newInstalledPackage = await this.communityPackagesService.updateNpmModule( + this.communityPackagesService.parseNpmPackageName(name).packageName, previouslyInstalledPackage, ); diff --git a/packages/cli/src/controllers/index.ts b/packages/cli/src/controllers/index.ts index 2091b95a30..4ccee47717 100644 --- a/packages/cli/src/controllers/index.ts +++ b/packages/cli/src/controllers/index.ts @@ -2,7 +2,6 @@ export { AuthController } from './auth.controller'; export { LdapController } from './ldap.controller'; export { MeController } from './me.controller'; export { MFAController } from './mfa.controller'; -export { NodesController } from './nodes.controller'; export { NodeTypesController } from './nodeTypes.controller'; export { OwnerController } from './owner.controller'; export { PasswordResetController } from './passwordReset.controller'; diff --git a/packages/cli/src/services/communityPackage.service.ts b/packages/cli/src/services/communityPackages.service.ts similarity index 68% rename from packages/cli/src/services/communityPackage.service.ts rename to packages/cli/src/services/communityPackages.service.ts index d373e12167..a1fc549d1d 100644 --- a/packages/cli/src/services/communityPackage.service.ts +++ b/packages/cli/src/services/communityPackages.service.ts @@ -1,13 +1,14 @@ import { exec } from 'child_process'; 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 { promisify } from 'util'; 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 { InstalledPackagesRepository } from '@/databases/repositories/installedPackages.repository'; import type { InstalledPackages } from '@/databases/entities/InstalledPackages'; @@ -18,11 +19,8 @@ import { RESPONSE_ERROR_MESSAGES, UNKNOWN_FAILURE_REASON, } from '@/constants'; - -import type { PublicInstalledPackage } from 'n8n-workflow'; -import type { PackageDirectoryLoader } from 'n8n-core'; import type { CommunityPackages } from '@/Interfaces'; -import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; const { PACKAGE_NAME_NOT_PROVIDED, @@ -45,8 +43,17 @@ const asyncExec = promisify(exec); const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/; @Service() -export class CommunityPackageService { - constructor(private readonly installedPackageRepository: InstalledPackagesRepository) {} +export class CommunityPackagesService { + missingPackages: string[] = []; + + constructor( + private readonly installedPackageRepository: InstalledPackagesRepository, + private readonly loadNodesAndCredentials: LoadNodesAndCredentials, + ) {} + + get hasMissingPackages() { + return this.missingPackages.length > 0; + } async findInstalledPackage(packageName: string) { return this.installedPackageRepository.findOne({ @@ -173,9 +180,8 @@ export class CommunityPackageService { }, []); } - matchMissingPackages(installedPackages: PublicInstalledPackage[], missingPackages: string) { - const missingPackagesList = missingPackages - .split(' ') + matchMissingPackages(installedPackages: PublicInstalledPackage[]) { + const missingPackagesList = this.missingPackages .map((name) => { try { // Strip away versions but maintain scope and package name @@ -221,45 +227,34 @@ export class CommunityPackageService { } hasPackageLoaded(packageName: string) { - const missingPackages = config.get('nodes.packagesMissing') as string | undefined; + if (!this.missingPackages.length) return true; - if (!missingPackages) return true; - - return !missingPackages - .split(' ') - .some( - (packageNameAndVersion) => - packageNameAndVersion.startsWith(packageName) && - packageNameAndVersion.replace(packageName, '').startsWith('@'), - ); + return !this.missingPackages.some( + (packageNameAndVersion) => + packageNameAndVersion.startsWith(packageName) && + packageNameAndVersion.replace(packageName, '').startsWith('@'), + ); } removePackageFromMissingList(packageName: string) { try { - const failedPackages = config.get('nodes.packagesMissing').split(' '); - - const packageFailedToLoad = failedPackages.filter( + this.missingPackages = this.missingPackages.filter( (packageNameAndVersion) => !packageNameAndVersion.startsWith(packageName) || !packageNameAndVersion.replace(packageName, '').startsWith('@'), ); - - config.set('nodes.packagesMissing', packageFailedToLoad.join(' ')); } catch { // do nothing } } - async setMissingPackages( - loadNodesAndCredentials: LoadNodesAndCredentials, - { reinstallMissingPackages }: { reinstallMissingPackages: boolean }, - ) { + async setMissingPackages({ reinstallMissingPackages }: { reinstallMissingPackages: boolean }) { const installedPackages = await this.getAllInstalledPackages(); const missingPackages = new Set<{ packageName: string; version: string }>(); installedPackages.forEach((installedPackage) => { 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. missingPackages.add({ packageName: installedPackage.packageName, @@ -269,7 +264,7 @@ export class CommunityPackageService { }); }); - config.set('nodes.packagesMissing', ''); + this.missingPackages = []; if (missingPackages.size === 0) return; @@ -283,10 +278,7 @@ export class CommunityPackageService { // Optimistic approach - stop if any installation fails for (const missingPackage of missingPackages) { - await loadNodesAndCredentials.installNpmModule( - missingPackage.packageName, - missingPackage.version, - ); + await this.installNpmModule(missingPackage.packageName, missingPackage.version); missingPackages.delete(missingPackage); } @@ -296,11 +288,79 @@ export class CommunityPackageService { } } - config.set( - 'nodes.packagesMissing', - Array.from(missingPackages) - .map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`) - .join(' '), + this.missingPackages = [...missingPackages].map( + (missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`, ); } + + async installNpmModule(packageName: string, version?: string): Promise { + return this.installOrUpdateNpmModule(packageName, { version }); + } + + async updateNpmModule( + packageName: string, + installedPackage: InstalledPackages, + ): Promise { + return this.installOrUpdateNpmModule(packageName, { installedPackage }); + } + + async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise { + 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); + } + } } diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts new file mode 100644 index 0000000000..613b716e90 --- /dev/null +++ b/packages/cli/src/services/frontend.service.ts @@ -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); + } + } + } +} diff --git a/packages/cli/test/integration/audit/nodes.risk.test.ts b/packages/cli/test/integration/audit/nodes.risk.test.ts index dbe414a951..6be3778570 100644 --- a/packages/cli/test/integration/audit/nodes.risk.test.ts +++ b/packages/cli/test/integration/audit/nodes.risk.test.ts @@ -8,7 +8,7 @@ import { toReportTitle } from '@/audit/utils'; import { mockInstance } from '../shared/utils/'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { NodeTypes } from '@/NodeTypes'; -import { CommunityPackageService } from '@/services/communityPackage.service'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; import Container from 'typedi'; import { LoggerProxy } from 'n8n-workflow'; @@ -19,8 +19,8 @@ LoggerProxy.init(getLogger()); const nodesAndCredentials = mockInstance(LoadNodesAndCredentials); nodesAndCredentials.getCustomDirectories.mockReturnValue([]); mockInstance(NodeTypes); -const communityPackageService = mockInstance(CommunityPackageService); -Container.set(CommunityPackageService, communityPackageService); +const communityPackagesService = mockInstance(CommunityPackagesService); +Container.set(CommunityPackagesService, communityPackagesService); beforeAll(async () => { await testDb.init(); @@ -36,7 +36,7 @@ afterAll(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) => { 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 () => { - communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); + communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); await saveManualTriggerWorkflow(); const testAudit = await audit(['nodes']); @@ -96,7 +96,7 @@ test('should not report non-risky official nodes', async () => { }); test('should report community nodes', async () => { - communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); + communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE); const testAudit = await audit(['nodes']); diff --git a/packages/cli/test/integration/commands/import.cmd.test.ts b/packages/cli/test/integration/commands/import.cmd.test.ts index a191267f62..e2a6039050 100644 --- a/packages/cli/test/integration/commands/import.cmd.test.ts +++ b/packages/cli/test/integration/commands/import.cmd.test.ts @@ -1,16 +1,18 @@ -import * as testDb from '../shared/testDb'; -import { mockInstance } from '../shared/utils/'; +import * as Config from '@oclif/config'; +import { mock } from 'jest-mock-extended'; +import { type ILogger, LoggerProxy } from 'n8n-workflow'; + import { InternalHooks } from '@/InternalHooks'; 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'; -import { getLogger } from '@/Logger'; - -LoggerProxy.init(getLogger()); +LoggerProxy.init(mock()); beforeAll(async () => { mockInstance(InternalHooks); + mockInstance(LoadNodesAndCredentials); await testDb.init(); }); diff --git a/packages/cli/test/integration/nodes.api.test.ts b/packages/cli/test/integration/community-packages.api.test.ts similarity index 51% rename from packages/cli/test/integration/nodes.api.test.ts rename to packages/cli/test/integration/community-packages.api.test.ts index 5c1e42b697..24fb59dfde 100644 --- a/packages/cli/test/integration/nodes.api.test.ts +++ b/packages/cli/test/integration/community-packages.api.test.ts @@ -1,8 +1,11 @@ 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 { Push } from '@/push'; -import { CommunityPackageService } from '@/services/communityPackage.service'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; import * as testDb from './shared/testDb'; @@ -14,15 +17,13 @@ import { mockPackageName, } from './shared/utils'; -import type { InstalledPackages } from '@db/entities/InstalledPackages'; -import type { InstalledNodes } from '@db/entities/InstalledNodes'; -import type { SuperAgentTest } from 'supertest'; - -const communityPackageService = mockInstance(CommunityPackageService); -const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials); +const communityPackagesService = mockInstance(CommunityPackagesService, { + hasMissingPackages: false, +}); +mockInstance(LoadNodesAndCredentials); mockInstance(Push); -const testServer = setupTestServer({ endpointGroups: ['nodes'] }); +const testServer = setupTestServer({ endpointGroups: ['community-packages'] }); const commonUpdatesProps = { createdAt: new Date(), @@ -47,12 +48,12 @@ beforeEach(() => { jest.resetAllMocks(); }); -describe('GET /nodes', () => { +describe('GET /community-packages', () => { test('should respond 200 if no nodes are installed', async () => { - communityPackageService.getAllInstalledPackages.mockResolvedValue([]); + communityPackagesService.getAllInstalledPackages.mockResolvedValue([]); const { body: { data }, - } = await authAgent.get('/nodes').expect(200); + } = await authAgent.get('/community-packages').expect(200); expect(data).toHaveLength(0); }); @@ -61,12 +62,12 @@ describe('GET /nodes', () => { const pkg = mockPackage(); const node = mockNode(pkg.packageName); pkg.installedNodes = [node]; - communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]); - communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]); + communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]); + communityPackagesService.matchPackagesWithUpdates.mockReturnValue([pkg]); const { body: { data }, - } = await authAgent.get('/nodes').expect(200); + } = await authAgent.get('/community-packages').expect(200); expect(data).toHaveLength(1); expect(data[0].installedNodes).toHaveLength(1); @@ -80,9 +81,9 @@ describe('GET /nodes', () => { const nodeB = 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, packageName: pkgA.packageName, @@ -97,7 +98,7 @@ describe('GET /nodes', () => { const { body: { data }, - } = await authAgent.get('/nodes').expect(200); + } = await authAgent.get('/community-packages').expect(200); expect(data).toHaveLength(2); @@ -110,26 +111,26 @@ describe('GET /nodes', () => { }); 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 () => { - 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 }]; - expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args); + expect(communityPackagesService.executeNpmCommand).toHaveBeenCalledWith(...args); }); test('should report package updates if available', async () => { const pkg = mockPackage(); - communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]); + communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]); - communityPackageService.executeNpmCommand.mockImplementation(() => { + communityPackagesService.executeNpmCommand.mockImplementation(() => { throw { code: 1, stdout: JSON.stringify({ @@ -143,7 +144,7 @@ describe('GET /nodes', () => { }; }); - communityPackageService.matchPackagesWithUpdates.mockReturnValue([ + communityPackagesService.matchPackagesWithUpdates.mockReturnValue([ { packageName: 'test', installedNodes: [], @@ -153,7 +154,7 @@ describe('GET /nodes', () => { const { body: { data }, - } = await authAgent.get('/nodes').expect(200); + } = await authAgent.get('/community-packages').expect(200); 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 () => { - await authAgent.post('/nodes').expect(400); + await authAgent.post('/community-packages').expect(400); }); test('should reject if package is duplicate', async () => { - communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); - communityPackageService.isPackageInstalled.mockResolvedValue(true); - communityPackageService.hasPackageLoaded.mockReturnValue(true); - communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); + communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); + communityPackagesService.isPackageInstalled.mockResolvedValue(true); + communityPackagesService.hasPackageLoaded.mockReturnValue(true); + communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); const { 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'); }); test('should allow installing packages that could not be loaded', async () => { - communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); - communityPackageService.hasPackageLoaded.mockReturnValue(false); - communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' }); - communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); - mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage()); + communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); + communityPackagesService.hasPackageLoaded.mockReturnValue(false); + communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' }); + communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); + 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 () => { - communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' }); - communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); + communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' }); + communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); const { 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'); }); }); -describe('DELETE /nodes', () => { +describe('DELETE /community-packages', () => { 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 () => { const { 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'); }); 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 () => { - await authAgent.patch('/nodes').expect(400); + await authAgent.patch('/community-packages').expect(400); }); test('should reject if package is not installed', async () => { const { 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'); }); test('should update a package', async () => { - communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage()); - communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName); + communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage()); + 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); }); }); diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index f100daf4ae..10b39e52f7 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -22,7 +22,7 @@ export type EndpointGroup = | 'credentials' | 'workflows' | 'publicApi' - | 'nodes' + | 'community-packages' | 'ldap' | 'saml' | 'sourceControl' diff --git a/packages/cli/test/integration/shared/utils/testServer.ts b/packages/cli/test/integration/shared/utils/testServer.ts index 6bcbc60bcc..33fb220a04 100644 --- a/packages/cli/test/integration/shared/utils/testServer.ts +++ b/packages/cli/test/integration/shared/utils/testServer.ts @@ -25,7 +25,6 @@ import { LdapController, MFAController, MeController, - NodesController, OwnerController, PasswordResetController, TagsController, @@ -34,12 +33,10 @@ import { import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares'; import { InternalHooks } from '@/InternalHooks'; -import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { PostHogClient } from '@/posthog'; import { variablesController } from '@/environments/variables/variables.controller'; import { LdapManager } from '@/Ldap/LdapManager.ee'; import { handleLdapInit } from '@/Ldap/helpers'; -import { Push } from '@/push'; import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers'; import { SamlController } from '@/sso/saml/routes/saml.controller.ee'; import { EventBusController } from '@/eventbus/eventBus.controller'; @@ -242,17 +239,11 @@ export const setupTestServer = ({ case 'sourceControl': registerController(app, config, Container.get(SourceControlController)); break; - case 'nodes': - registerController( - app, - config, - new NodesController( - config, - Container.get(LoadNodesAndCredentials), - Container.get(Push), - internalHooks, - ), + case 'community-packages': + const { CommunityPackagesController } = await import( + '@/controllers/communityPackages.controller' ); + registerController(app, config, Container.get(CommunityPackagesController)); case 'me': registerController( app, diff --git a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts index 0ba430b820..fe246e104b 100644 --- a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts @@ -1,7 +1,8 @@ import { v4 as uuid } from 'uuid'; 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 { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -11,22 +12,19 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { Role } from '@db/entities/Role'; import { User } from '@db/entities/User'; import { getLogger } from '@/Logger'; -import { randomEmail, randomName } from '../integration/shared/random'; -import * as Helpers from './Helpers'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; - import { WorkflowRunner } from '@/WorkflowRunner'; -import { mock } from 'jest-mock-extended'; -import type { ExternalHooks } from '@/ExternalHooks'; -import { Container } from 'typedi'; +import { ExternalHooks } from '@/ExternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; -import { mockInstance } from '../integration/shared/utils/'; import { Push } from '@/push'; import { ActiveExecutions } from '@/ActiveExecutions'; -import { NodeTypes } from '@/NodeTypes'; import { SecretsHelper } from '@/SecretsHelpers'; 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: @@ -114,13 +112,6 @@ jest.mock('@/Db', () => { return fakeQueryBuilder; }), }, - Webhook: { - clear: jest.fn(), - delete: jest.fn(), - }, - Variables: { - find: jest.fn(() => []), - }, }, }; }); @@ -140,37 +131,24 @@ const workflowExecuteAdditionalDataExecuteErrorWorkflowSpy = jest.spyOn( ); describe('ActiveWorkflowRunner', () => { - let externalHooks: ExternalHooks; - let activeWorkflowRunner: ActiveWorkflowRunner; + mockInstance(ActiveExecutions); + const externalHooks = mockInstance(ExternalHooks); 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 () => { LoggerProxy.init(getLogger()); - const nodesAndCredentials: INodesAndCredentials = { - 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, - ); + variablesService.getAllCached.mockResolvedValue([]); }); afterEach(async () => { diff --git a/packages/cli/test/unit/CredentialTypes.test.ts b/packages/cli/test/unit/CredentialTypes.test.ts index bf188989ca..731e1b2bac 100644 --- a/packages/cli/test/unit/CredentialTypes.test.ts +++ b/packages/cli/test/unit/CredentialTypes.test.ts @@ -1,36 +1,29 @@ -import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow'; import { CredentialTypes } from '@/CredentialTypes'; import { Container } from 'typedi'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { mockInstance } from '../integration/shared/utils'; describe('CredentialTypes', () => { - const mockNodesAndCredentials: INodesAndCredentials = { - loaded: { - nodes: {}, - credentials: { - fakeFirstCredential: { - type: { - name: 'fakeFirstCredential', - displayName: 'Fake First Credential', - properties: [], - }, - sourcePath: '', + const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, { + loadedCredentials: { + fakeFirstCredential: { + type: { + name: 'fakeFirstCredential', + displayName: 'Fake First Credential', + properties: [], }, - fakeSecondCredential: { - type: { - name: 'fakeSecondCredential', - displayName: 'Fake Second Credential', - properties: [], - }, - sourcePath: '', + sourcePath: '', + }, + fakeSecondCredential: { + type: { + name: 'fakeSecondCredential', + displayName: 'Fake Second Credential', + properties: [], }, + sourcePath: '', }, }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, - }; - - Container.set(LoadNodesAndCredentials, mockNodesAndCredentials); + }); const credentialTypes = Container.get(CredentialTypes); @@ -39,7 +32,7 @@ describe('CredentialTypes', () => { }); test('Should return correct credential type for valid name', () => { - const mockedCredentialTypes = mockNodesAndCredentials.loaded.credentials; + const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials; expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual( mockedCredentialTypes.fakeFirstCredential.type, ); diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/test/unit/CredentialsHelper.test.ts index 5a11254cdb..96907b725b 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/test/unit/CredentialsHelper.test.ts @@ -2,68 +2,58 @@ import type { IAuthenticateGeneric, ICredentialDataDecryptedObject, ICredentialType, - ICredentialTypes, IHttpRequestOptions, INode, INodeProperties, - INodesAndCredentials, } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import { Workflow } from 'n8n-workflow'; import { CredentialsHelper } from '@/CredentialsHelper'; -import { CredentialTypes } from '@/CredentialTypes'; -import { Container } from 'typedi'; import { NodeTypes } from '@/NodeTypes'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; +import { mockInstance } from '../integration/shared/utils'; describe('CredentialsHelper', () => { const TEST_ENCRYPTION_KEY = 'test'; - const mockNodesAndCredentials: INodesAndCredentials = { - loaded: { - nodes: { - 'test.set': { - sourcePath: '', - type: { - description: { - displayName: 'Set', - name: 'set', - group: ['input'], - version: 1, - description: 'Sets a value', - defaults: { - name: 'Set', - color: '#0000FF', - }, - inputs: ['main'], - outputs: ['main'], - properties: [ - { - displayName: 'Value1', - name: 'value1', - type: 'string', - default: 'default-value1', - }, - { - displayName: 'Value2', - name: 'value2', - type: 'string', - default: 'default-value2', - }, - ], + const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, { + loadedNodes: { + 'test.set': { + sourcePath: '', + type: { + description: { + displayName: 'Set', + name: 'set', + group: ['input'], + version: 1, + description: 'Sets a value', + defaults: { + name: 'Set', + color: '#0000FF', }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Value1', + name: 'value1', + type: 'string', + default: 'default-value1', + }, + { + displayName: 'Value2', + name: 'value2', + type: 'string', + default: 'default-value2', + }, + ], }, }, }, - credentials: {}, }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, - }; + }); - Container.set(LoadNodesAndCredentials, mockNodesAndCredentials); - - const nodeTypes = Container.get(NodeTypes); + const nodeTypes = mockInstance(NodeTypes); describe('authenticate', () => { const tests: Array<{ @@ -280,20 +270,14 @@ describe('CredentialsHelper', () => { for (const testData of tests) { test(testData.description, async () => { - mockNodesAndCredentials.loaded.credentials = { + mockNodesAndCredentials.loadedCredentials = { [testData.input.credentialType.name]: { type: testData.input.credentialType, sourcePath: '', }, }; - const credentialTypes = Container.get(CredentialTypes); - - const credentialsHelper = new CredentialsHelper( - TEST_ENCRYPTION_KEY, - credentialTypes, - nodeTypes, - ); + const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY); const result = await credentialsHelper.authenticate( testData.input.credentials, diff --git a/packages/cli/test/unit/PermissionChecker.test.ts b/packages/cli/test/unit/PermissionChecker.test.ts index 139cf05710..dab85820d2 100644 --- a/packages/cli/test/unit/PermissionChecker.test.ts +++ b/packages/cli/test/unit/PermissionChecker.test.ts @@ -1,7 +1,8 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; -import type { ICredentialTypes, INodeTypes } from 'n8n-workflow'; -import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; +import { mock } from 'jest-mock-extended'; +import type { ILogger, INodeTypes } from 'n8n-workflow'; +import { LoggerProxy, SubworkflowOperationError, Workflow } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; @@ -14,35 +15,26 @@ import { UserService } from '@/services/user.service'; import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import { WorkflowsService } from '@/workflows/workflows.services'; +import { OwnershipService } from '@/services/ownership.service'; +import { mockInstance } from '../integration/shared/utils/'; import { randomCredentialPayload as randomCred, randomPositiveDigit, } from '../integration/shared/random'; import * as testDb from '../integration/shared/testDb'; -import { mockNodeTypesData } from './Helpers'; import type { SaveCredentialFunction } from '../integration/shared/types'; -import { mockInstance } from '../integration/shared/utils/'; -import { OwnershipService } from '@/services/ownership.service'; +import { mockNodeTypesData } from './Helpers'; -import { LoggerProxy } from 'n8n-workflow'; -import { getLogger } from '@/Logger'; - -LoggerProxy.init(getLogger()); +LoggerProxy.init(mock()); let mockNodeTypes: INodeTypes; let credentialOwnerRole: Role; let workflowOwnerRole: Role; let saveCredential: SaveCredentialFunction; -const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']); mockInstance(LoadNodesAndCredentials, { - loaded: { - nodes: MOCK_NODE_TYPES_DATA, - credentials: {}, - }, - known: { nodes: {}, credentials: {} }, - credentialTypes: {} as ICredentialTypes, + loadedNodes: mockNodeTypesData(['start', 'actionNetwork']), }); beforeAll(async () => { diff --git a/packages/cli/test/unit/services/communityPackage.service.test.ts b/packages/cli/test/unit/services/communityPackages.service.test.ts similarity index 71% rename from packages/cli/test/unit/services/communityPackage.service.test.ts rename to packages/cli/test/unit/services/communityPackages.service.test.ts index e0b3f894bb..6bbee36d53 100644 --- a/packages/cli/test/unit/services/communityPackage.service.test.ts +++ b/packages/cli/test/unit/services/communityPackages.service.test.ts @@ -1,7 +1,9 @@ import { exec } from 'child_process'; import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises'; - import axios from 'axios'; +import { mocked } from 'jest-mock'; +import Container from 'typedi'; +import type { PublicInstalledPackage } from 'n8n-workflow'; import { NODE_PACKAGE_PREFIX, @@ -9,22 +11,20 @@ import { NPM_PACKAGE_STATUS_GOOD, RESPONSE_ERROR_MESSAGES, } from '@/constants'; -import { InstalledPackages } from '@db/entities/InstalledPackages'; -import { randomName } from '../../integration/shared/random'; import config from '@/config'; -import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils'; -import { mocked } from 'jest-mock'; - +import { InstalledPackages } from '@db/entities/InstalledPackages'; import type { CommunityPackages } from '@/Interfaces'; -import { CommunityPackageService } from '@/services/communityPackage.service'; +import { CommunityPackagesService } from '@/services/communityPackages.service'; import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories'; -import Container from 'typedi'; import { InstalledNodes } from '@/databases/entities/InstalledNodes'; +import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; + import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION, } 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('child_process'); @@ -38,10 +38,8 @@ const execMock = ((...args) => { cb(null, 'Done', ''); }) as typeof exec; -describe('CommunityPackageService', () => { +describe('CommunityPackagesService', () => { const installedNodesRepository = mockInstance(InstalledNodesRepository); - Container.set(InstalledNodesRepository, installedNodesRepository); - installedNodesRepository.create.mockImplementation(() => { const nodeName = randomName(); @@ -54,7 +52,6 @@ describe('CommunityPackageService', () => { }); const installedPackageRepository = mockInstance(InstalledPackagesRepository); - installedPackageRepository.create.mockImplementation(() => { return Object.assign(new InstalledPackages(), { packageName: mockPackageName(), @@ -62,7 +59,9 @@ describe('CommunityPackageService', () => { }); }); - const communityPackageService = new CommunityPackageService(installedPackageRepository); + mockInstance(LoadNodesAndCredentials); + + const communityPackagesService = Container.get(CommunityPackagesService); beforeEach(() => { config.load(config.default); @@ -70,18 +69,18 @@ describe('CommunityPackageService', () => { describe('parseNpmPackageName()', () => { test('should fail with empty package name', () => { - expect(() => communityPackageService.parseNpmPackageName('')).toThrowError(); + expect(() => communityPackagesService.parseNpmPackageName('')).toThrowError(); }); test('should fail with invalid package prefix name', () => { expect(() => - communityPackageService.parseNpmPackageName('INVALID_PREFIX@123'), + communityPackagesService.parseNpmPackageName('INVALID_PREFIX@123'), ).toThrowError(); }); test('should parse valid package name', () => { const name = mockPackageName(); - const parsed = communityPackageService.parseNpmPackageName(name); + const parsed = communityPackagesService.parseNpmPackageName(name); expect(parsed.rawString).toBe(name); expect(parsed.packageName).toBe(name); @@ -93,7 +92,7 @@ describe('CommunityPackageService', () => { const name = mockPackageName(); const version = '0.1.1'; const fullPackageName = `${name}@${version}`; - const parsed = communityPackageService.parseNpmPackageName(fullPackageName); + const parsed = communityPackagesService.parseNpmPackageName(fullPackageName); expect(parsed.rawString).toBe(fullPackageName); expect(parsed.packageName).toBe(name); @@ -106,7 +105,7 @@ describe('CommunityPackageService', () => { const name = mockPackageName(); const version = '0.1.1'; const fullPackageName = `${scope}/${name}@${version}`; - const parsed = communityPackageService.parseNpmPackageName(fullPackageName); + const parsed = communityPackagesService.parseNpmPackageName(fullPackageName); expect(parsed.rawString).toBe(fullPackageName); expect(parsed.packageName).toBe(`${scope}/${name}`); @@ -134,7 +133,7 @@ describe('CommunityPackageService', () => { mocked(exec).mockImplementation(execMock); - await communityPackageService.executeNpmCommand('ls'); + await communityPackagesService.executeNpmCommand('ls'); expect(fsAccess).toHaveBeenCalled(); expect(exec).toHaveBeenCalled(); @@ -144,7 +143,7 @@ describe('CommunityPackageService', () => { test('should make sure folder exists', async () => { mocked(exec).mockImplementation(execMock); - await communityPackageService.executeNpmCommand('ls'); + await communityPackagesService.executeNpmCommand('ls'); expect(fsAccess).toHaveBeenCalled(); expect(exec).toHaveBeenCalled(); expect(fsMkdir).toBeCalledTimes(0); @@ -156,7 +155,7 @@ describe('CommunityPackageService', () => { throw new Error('Folder does not exist.'); }); - await communityPackageService.executeNpmCommand('ls'); + await communityPackagesService.executeNpmCommand('ls'); expect(fsAccess).toHaveBeenCalled(); expect(exec).toHaveBeenCalled(); @@ -172,7 +171,7 @@ describe('CommunityPackageService', () => { 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); @@ -186,7 +185,7 @@ describe('CommunityPackageService', () => { test('should return same list if availableUpdates is undefined', () => { const fakePkgs = mockPackagePair(); - const crossedPkgs = communityPackageService.matchPackagesWithUpdates(fakePkgs); + const crossedPkgs = communityPackagesService.matchPackagesWithUpdates(fakePkgs); expect(crossedPkgs).toEqual(fakePkgs); }); @@ -210,7 +209,7 @@ describe('CommunityPackageService', () => { }; const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = - communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates); + communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates); expect(crossedPkgA.updateAvailable).toBe('0.2.0'); expect(crossedPkgB.updateAvailable).toBe('0.3.0'); @@ -229,7 +228,7 @@ describe('CommunityPackageService', () => { }; const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] = - communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates); + communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates); expect(crossedPkgA.updateAvailable).toBeUndefined(); expect(crossedPkgB.updateAvailable).toBe('0.3.0'); @@ -239,12 +238,12 @@ describe('CommunityPackageService', () => { describe('matchMissingPackages()', () => { test('should not match failed packages that do not exist', () => { 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( - fakePkgs, - notFoundPkgNames, - ); + const matchedPackages = communityPackagesService.matchMissingPackages(fakePkgs); expect(matchedPackages).toEqual(fakePkgs); @@ -256,12 +255,15 @@ describe('CommunityPackageService', () => { test('should match failed packages that should be present', () => { 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( - [pkgA, pkgB], - notFoundPkgNames, - ); + const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([ + pkgA, + pkgB, + ]); expect(matchedPkgA.failedLoading).toBe(true); expect(matchedPkgB.failedLoading).toBeUndefined(); @@ -269,11 +271,14 @@ describe('CommunityPackageService', () => { test('should match failed packages even if version is wrong', () => { 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`; - const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages( - [pkgA, pkgB], - notFoundPackageList, - ); + setMissingPackages([ + `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`, + `${pkgA.packageName}@123.456.789`, + ]); + const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([ + pkgA, + pkgB, + ]); expect(matchedPkgA.failedLoading).toBe(true); expect(matchedPkgB.failedLoading).toBeUndefined(); @@ -282,7 +287,7 @@ describe('CommunityPackageService', () => { describe('checkNpmPackageStatus()', () => { test('should call axios.post', async () => { - await communityPackageService.checkNpmPackageStatus(mockPackageName()); + await communityPackagesService.checkNpmPackageStatus(mockPackageName()); expect(axios.post).toHaveBeenCalled(); }); @@ -292,7 +297,7 @@ describe('CommunityPackageService', () => { 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); }); @@ -300,7 +305,7 @@ describe('CommunityPackageService', () => { test('should warn if package is banned', async () => { mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } }); - const result = (await communityPackageService.checkNpmPackageStatus( + const result = (await communityPackagesService.checkNpmPackageStatus( mockPackageName(), )) as CommunityPackages.PackageStatusCheck; @@ -311,47 +316,50 @@ describe('CommunityPackageService', () => { describe('hasPackageLoadedSuccessfully()', () => { test('should return true when failed package list does not exist', () => { - config.set('nodes.packagesMissing', undefined); - - expect(communityPackageService.hasPackageLoaded('package')).toBe(true); + setMissingPackages([]); + expect(communityPackagesService.hasPackageLoaded('package')).toBe(true); }); 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'); - - expect(communityPackageService.hasPackageLoaded('packageC')).toBe(true); + setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']); + expect(communityPackagesService.hasPackageLoaded('packageC')).toBe(true); }); 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'); - - expect(communityPackageService.hasPackageLoaded('packageA')).toBe(false); + setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']); + expect(communityPackagesService.hasPackageLoaded('packageA')).toBe(false); }); }); describe('removePackageFromMissingList()', () => { test('should do nothing if key does not exist', () => { - config.set('nodes.packagesMissing', undefined); + setMissingPackages([]); + communityPackagesService.removePackageFromMissingList('packageA'); - communityPackageService.removePackageFromMissingList('packageA'); - - expect(config.get('nodes.packagesMissing')).toBeUndefined(); + expect(communityPackagesService.missingPackages).toBeEmptyArray(); }); 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', () => { - const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageB@0.2.0'; - config.set('nodes.packagesMissing', failedToLoadList); - communityPackageService.removePackageFromMissingList('packageC'); + const failedToLoadList = ['packageA@0.1.0', 'packageB@0.2.0', 'packageB@0.2.0']; + setMissingPackages(failedToLoadList); + communityPackagesService.removePackageFromMissingList('packageC'); - expect(config.get('nodes.packagesMissing')).toBe(failedToLoadList); + expect(communityPackagesService.missingPackages).toEqual(failedToLoadList); }); }); + + const setMissingPackages = (missingPackages: string[]) => { + Object.assign(communityPackagesService, { missingPackages }); + }; }); diff --git a/packages/editor-ui/src/api/communityNodes.ts b/packages/editor-ui/src/api/communityNodes.ts index bb75161b92..12089e36fc 100644 --- a/packages/editor-ui/src/api/communityNodes.ts +++ b/packages/editor-ui/src/api/communityNodes.ts @@ -5,7 +5,7 @@ import { get, post, makeRestApiRequest } from '@/utils'; export async function getInstalledCommunityNodes( context: IRestApiContext, ): Promise { - const response = await get(context.baseUrl, '/nodes'); + const response = await get(context.baseUrl, '/community-packages'); return response.data || []; } @@ -13,16 +13,16 @@ export async function installNewPackage( context: IRestApiContext, name: string, ): Promise { - return post(context.baseUrl, '/nodes', { name }); + return post(context.baseUrl, '/community-packages', { name }); } export async function uninstallPackage(context: IRestApiContext, name: string): Promise { - return makeRestApiRequest(context, 'DELETE', '/nodes', { name }); + return makeRestApiRequest(context, 'DELETE', '/community-packages', { name }); } export async function updatePackage( context: IRestApiContext, name: string, ): Promise { - return makeRestApiRequest(context, 'PATCH', '/nodes', { name }); + return makeRestApiRequest(context, 'PATCH', '/community-packages', { name }); } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 7f2aad8282..1256a1df25 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1712,17 +1712,6 @@ type LoadedData = Record>; export type ICredentialTypeData = LoadedData; export type INodeTypeData = LoadedData; -export type LoadedNodesAndCredentials = { - nodes: INodeTypeData; - credentials: ICredentialTypeData; -}; - -export interface INodesAndCredentials { - known: KnownNodesAndCredentials; - loaded: LoadedNodesAndCredentials; - credentialTypes: ICredentialTypes; -} - export interface IRun { data: IRunExecutionData; finished?: boolean;