mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(core): Refactor nodes loading (no-changelog) (#7283)
fixes PAY-605
This commit is contained in:
parent
789e1e7ed4
commit
c5ee06cc61
|
@ -11,4 +11,3 @@ packages/**/.turbo
|
||||||
.github
|
.github
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
packages/cli/dist/**/e2e.*
|
packages/cli/dist/**/e2e.*
|
||||||
packages/cli/dist/ReloadNodesAndCredentials.*
|
|
||||||
|
|
|
@ -61,8 +61,7 @@
|
||||||
"templates",
|
"templates",
|
||||||
"dist",
|
"dist",
|
||||||
"oclif.manifest.json",
|
"oclif.manifest.json",
|
||||||
"!dist/**/e2e.*",
|
"!dist/**/e2e.*"
|
||||||
"!dist/ReloadNodesAndCredentials.*"
|
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apidevtools/swagger-cli": "4.0.0",
|
"@apidevtools/swagger-cli": "4.0.0",
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
import { Service } from 'typedi';
|
||||||
import { loadClassInIsolation } from 'n8n-core';
|
import { loadClassInIsolation } from 'n8n-core';
|
||||||
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
|
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CredentialTypes implements ICredentialTypes {
|
export class CredentialTypes implements ICredentialTypes {
|
||||||
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {
|
constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) {}
|
||||||
nodesAndCredentials.credentialTypes = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
recognizes(type: string) {
|
recognizes(type: string) {
|
||||||
return type in this.knownCredentials || type in this.loadedCredentials;
|
const { loadedCredentials, knownCredentials } = this.loadNodesAndCredentials;
|
||||||
|
return type in knownCredentials || type in loadedCredentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
getByName(credentialType: string): ICredentialType {
|
getByName(credentialType: string): ICredentialType {
|
||||||
|
@ -19,14 +18,14 @@ export class CredentialTypes implements ICredentialTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
getNodeTypesToTestWith(type: string): string[] {
|
getNodeTypesToTestWith(type: string): string[] {
|
||||||
return this.knownCredentials[type]?.nodesToTestWith ?? [];
|
return this.loadNodesAndCredentials.knownCredentials[type]?.nodesToTestWith ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all parent types of the given credential type
|
* Returns all parent types of the given credential type
|
||||||
*/
|
*/
|
||||||
getParentTypes(typeName: string): string[] {
|
getParentTypes(typeName: string): string[] {
|
||||||
const extendsArr = this.knownCredentials[typeName]?.extends ?? [];
|
const extendsArr = this.loadNodesAndCredentials.knownCredentials[typeName]?.extends ?? [];
|
||||||
if (extendsArr.length) {
|
if (extendsArr.length) {
|
||||||
extendsArr.forEach((type) => {
|
extendsArr.forEach((type) => {
|
||||||
extendsArr.push(...this.getParentTypes(type));
|
extendsArr.push(...this.getParentTypes(type));
|
||||||
|
@ -36,12 +35,11 @@ export class CredentialTypes implements ICredentialTypes {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCredential(type: string): LoadedClass<ICredentialType> {
|
private getCredential(type: string): LoadedClass<ICredentialType> {
|
||||||
const loadedCredentials = this.loadedCredentials;
|
const { loadedCredentials, knownCredentials } = this.loadNodesAndCredentials;
|
||||||
if (type in loadedCredentials) {
|
if (type in loadedCredentials) {
|
||||||
return loadedCredentials[type];
|
return loadedCredentials[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownCredentials = this.knownCredentials;
|
|
||||||
if (type in knownCredentials) {
|
if (type in knownCredentials) {
|
||||||
const { className, sourcePath } = knownCredentials[type];
|
const { className, sourcePath } = knownCredentials[type];
|
||||||
const loaded: ICredentialType = loadClassInIsolation(sourcePath, className);
|
const loaded: ICredentialType = loadClassInIsolation(sourcePath, className);
|
||||||
|
@ -50,12 +48,4 @@ export class CredentialTypes implements ICredentialTypes {
|
||||||
}
|
}
|
||||||
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`);
|
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get loadedCredentials() {
|
|
||||||
return this.nodesAndCredentials.loaded.credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get knownCredentials() {
|
|
||||||
return this.nodesAndCredentials.known.credentials;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
@ -89,13 +88,11 @@ const mockNodeTypes: INodeTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CredentialsHelper extends ICredentialsHelper {
|
export class CredentialsHelper extends ICredentialsHelper {
|
||||||
constructor(
|
private credentialTypes = Container.get(CredentialTypes);
|
||||||
encryptionKey: string,
|
|
||||||
private credentialTypes = Container.get(CredentialTypes),
|
private nodeTypes = Container.get(NodeTypes);
|
||||||
private nodeTypes = Container.get(NodeTypes),
|
|
||||||
) {
|
private credentialsOverwrites = Container.get(CredentialsOverwrites);
|
||||||
super(encryptionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the required authentication information to the request
|
* Add the required authentication information to the request
|
||||||
|
@ -388,7 +385,10 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
const credentialsProperties = this.getCredentialsProperties(type);
|
const credentialsProperties = this.getCredentialsProperties(type);
|
||||||
|
|
||||||
// Load and apply the credentials overwrites if any exist
|
// Load and apply the credentials overwrites if any exist
|
||||||
const dataWithOverwrites = CredentialsOverwrites().applyOverwrite(type, decryptedDataOriginal);
|
const dataWithOverwrites = this.credentialsOverwrites.applyOverwrite(
|
||||||
|
type,
|
||||||
|
decryptedDataOriginal,
|
||||||
|
);
|
||||||
|
|
||||||
// Add the default credential values
|
// Add the default credential values
|
||||||
let decryptedData = NodeHelpers.getNodeParameters(
|
let decryptedData = NodeHelpers.getNodeParameters(
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import config from '@/config';
|
import { Service } from 'typedi';
|
||||||
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
||||||
import { deepCopy, LoggerProxy as Logger, jsonParse, ICredentialTypes } from 'n8n-workflow';
|
import { deepCopy, LoggerProxy as Logger, jsonParse } from 'n8n-workflow';
|
||||||
|
import config from '@/config';
|
||||||
import type { ICredentialsOverwrite } from '@/Interfaces';
|
import type { ICredentialsOverwrite } from '@/Interfaces';
|
||||||
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
|
|
||||||
class CredentialsOverwritesClass {
|
@Service()
|
||||||
|
export class CredentialsOverwrites {
|
||||||
private overwriteData: ICredentialsOverwrite = {};
|
private overwriteData: ICredentialsOverwrite = {};
|
||||||
|
|
||||||
private resolvedTypes: string[] = [];
|
private resolvedTypes: string[] = [];
|
||||||
|
|
||||||
constructor(private credentialTypes: ICredentialTypes) {
|
constructor(private credentialTypes: CredentialTypes) {
|
||||||
const data = config.getEnv('credentials.overwrite.data');
|
const data = config.getEnv('credentials.overwrite.data');
|
||||||
const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
|
const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
|
||||||
errorMessage: 'The credentials-overwrite is not valid JSON.',
|
errorMessage: 'The credentials-overwrite is not valid JSON.',
|
||||||
|
@ -96,20 +99,3 @@ class CredentialsOverwritesClass {
|
||||||
return this.overwriteData;
|
return this.overwriteData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
export function CredentialsOverwrites(
|
|
||||||
credentialTypes?: ICredentialTypes,
|
|
||||||
): CredentialsOverwritesClass {
|
|
||||||
if (!credentialsOverwritesInstance) {
|
|
||||||
if (credentialTypes) {
|
|
||||||
credentialsOverwritesInstance = new CredentialsOverwritesClass(credentialTypes);
|
|
||||||
} else {
|
|
||||||
throw new Error('CredentialsOverwrites not initialized yet');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentialsOverwritesInstance;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import uniq from 'lodash/uniq';
|
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
|
import { Container, Service } from 'typedi';
|
||||||
|
import path from 'path';
|
||||||
|
import fsPromises from 'fs/promises';
|
||||||
|
|
||||||
import type { DirectoryLoader, Types } from 'n8n-core';
|
import type { DirectoryLoader, Types } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
CUSTOM_EXTENSION_ENV,
|
CUSTOM_EXTENSION_ENV,
|
||||||
|
@ -9,36 +12,30 @@ import {
|
||||||
LazyPackageDirectoryLoader,
|
LazyPackageDirectoryLoader,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ICredentialTypes,
|
|
||||||
ILogger,
|
|
||||||
INodesAndCredentials,
|
|
||||||
KnownNodesAndCredentials,
|
KnownNodesAndCredentials,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
LoadedNodesAndCredentials,
|
INodeTypeData,
|
||||||
|
ICredentialTypeData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||||
|
|
||||||
import { createWriteStream } from 'fs';
|
|
||||||
import { mkdir } from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
|
||||||
import { CommunityPackageService } from './services/communityPackage.service';
|
|
||||||
import {
|
import {
|
||||||
GENERATED_STATIC_DIR,
|
|
||||||
RESPONSE_ERROR_MESSAGES,
|
|
||||||
CUSTOM_API_CALL_KEY,
|
CUSTOM_API_CALL_KEY,
|
||||||
CUSTOM_API_CALL_NAME,
|
CUSTOM_API_CALL_NAME,
|
||||||
inTest,
|
inTest,
|
||||||
CLI_DIR,
|
CLI_DIR,
|
||||||
inE2ETests,
|
inE2ETests,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
|
||||||
import Container, { Service } from 'typedi';
|
interface LoadedNodesAndCredentials {
|
||||||
|
nodes: INodeTypeData;
|
||||||
|
credentials: ICredentialTypeData;
|
||||||
|
}
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class LoadNodesAndCredentials implements INodesAndCredentials {
|
export class LoadNodesAndCredentials {
|
||||||
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
private known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
||||||
|
|
||||||
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
|
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
|
||||||
|
|
||||||
|
@ -50,20 +47,20 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
|
|
||||||
includeNodes = config.getEnv('nodes.include');
|
includeNodes = config.getEnv('nodes.include');
|
||||||
|
|
||||||
credentialTypes: ICredentialTypes;
|
|
||||||
|
|
||||||
logger: ILogger;
|
|
||||||
|
|
||||||
private downloadFolder: string;
|
private downloadFolder: string;
|
||||||
|
|
||||||
|
private postProcessors: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
if (inTest) throw new Error('Not available in tests');
|
||||||
|
|
||||||
// Make sure the imported modules can resolve dependencies fine.
|
// Make sure the imported modules can resolve dependencies fine.
|
||||||
const delimiter = process.platform === 'win32' ? ';' : ':';
|
const delimiter = process.platform === 'win32' ? ';' : ':';
|
||||||
process.env.NODE_PATH = module.paths.join(delimiter);
|
process.env.NODE_PATH = module.paths.join(delimiter);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
if (!inTest) module.constructor._initPaths();
|
module.constructor._initPaths();
|
||||||
|
|
||||||
if (!inE2ETests) {
|
if (!inE2ETests) {
|
||||||
this.excludeNodes = this.excludeNodes ?? [];
|
this.excludeNodes = this.excludeNodes ?? [];
|
||||||
|
@ -91,48 +88,30 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
|
|
||||||
await this.loadNodesFromCustomDirectories();
|
await this.loadNodesFromCustomDirectories();
|
||||||
await this.postProcessLoaders();
|
await this.postProcessLoaders();
|
||||||
this.injectCustomApiCallOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateTypesForFrontend() {
|
addPostProcessor(fn: () => Promise<void>) {
|
||||||
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
this.postProcessors.push(fn);
|
||||||
for (const credential of this.types.credentials) {
|
|
||||||
const overwrittenProperties = [];
|
|
||||||
this.credentialTypes
|
|
||||||
.getParentTypes(credential.name)
|
|
||||||
.reverse()
|
|
||||||
.map((name) => credentialsOverwrites[name])
|
|
||||||
.forEach((overwrite) => {
|
|
||||||
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (credential.name in credentialsOverwrites) {
|
|
||||||
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overwrittenProperties.length) {
|
isKnownNode(type: string) {
|
||||||
credential.__overwrittenProperties = uniq(overwrittenProperties);
|
return type in this.known.nodes;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-render all the node and credential types as static json files
|
get loadedCredentials() {
|
||||||
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true });
|
return this.loaded.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
const writeStaticJSON = async (name: string, data: object[]) => {
|
get loadedNodes() {
|
||||||
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`);
|
return this.loaded.nodes;
|
||||||
const stream = createWriteStream(filePath, 'utf-8');
|
}
|
||||||
stream.write('[\n');
|
|
||||||
data.forEach((entry, index) => {
|
|
||||||
stream.write(JSON.stringify(entry));
|
|
||||||
if (index !== data.length - 1) stream.write(',');
|
|
||||||
stream.write('\n');
|
|
||||||
});
|
|
||||||
stream.write(']\n');
|
|
||||||
stream.end();
|
|
||||||
};
|
|
||||||
|
|
||||||
await writeStaticJSON('nodes', this.types.nodes);
|
get knownCredentials() {
|
||||||
await writeStaticJSON('credentials', this.types.credentials);
|
return this.known.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
get knownNodes() {
|
||||||
|
return this.known.nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadNodesFromNodeModules(
|
private async loadNodesFromNodeModules(
|
||||||
|
@ -163,6 +142,18 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolveIcon(packageName: string, url: string): string | undefined {
|
||||||
|
const loader = this.loaders[packageName];
|
||||||
|
if (loader) {
|
||||||
|
const pathPrefix = `/icons/${packageName}/`;
|
||||||
|
const filePath = path.resolve(loader.directory, url.substring(pathPrefix.length));
|
||||||
|
if (!path.relative(loader.directory, filePath).includes('..')) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getCustomDirectories(): string[] {
|
getCustomDirectories(): string[] {
|
||||||
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
|
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
|
||||||
|
|
||||||
|
@ -180,93 +171,16 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async installOrUpdateNpmModule(
|
async loadPackage(packageName: string) {
|
||||||
packageName: string,
|
|
||||||
options: { version?: string } | { installedPackage: InstalledPackages },
|
|
||||||
) {
|
|
||||||
const isUpdate = 'installedPackage' in options;
|
|
||||||
const command = isUpdate
|
|
||||||
? `npm update ${packageName}`
|
|
||||||
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
|
|
||||||
|
|
||||||
const communityPackageService = Container.get(CommunityPackageService);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await communityPackageService.executeNpmCommand(command);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
|
||||||
throw new Error(`The npm package "${packageName}" could not be found.`);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
|
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
|
||||||
|
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
||||||
let loader: PackageDirectoryLoader;
|
|
||||||
try {
|
|
||||||
loader = await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
|
||||||
} catch (error) {
|
|
||||||
// Remove this package since loading it failed
|
|
||||||
const removeCommand = `npm remove ${packageName}`;
|
|
||||||
try {
|
|
||||||
await communityPackageService.executeNpmCommand(removeCommand);
|
|
||||||
} catch {}
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loader.loadedNodes.length > 0) {
|
async unloadPackage(packageName: string) {
|
||||||
// Save info to DB
|
|
||||||
try {
|
|
||||||
if (isUpdate) {
|
|
||||||
await communityPackageService.removePackageFromDatabase(options.installedPackage);
|
|
||||||
}
|
|
||||||
const installedPackage = await communityPackageService.persistInstalledPackage(loader);
|
|
||||||
await this.postProcessLoaders();
|
|
||||||
await this.generateTypesForFrontend();
|
|
||||||
return installedPackage;
|
|
||||||
} catch (error) {
|
|
||||||
LoggerProxy.error('Failed to save installed packages and nodes', {
|
|
||||||
error: error as Error,
|
|
||||||
packageName,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Remove this package since it contains no loadable nodes
|
|
||||||
const removeCommand = `npm remove ${packageName}`;
|
|
||||||
try {
|
|
||||||
await communityPackageService.executeNpmCommand(removeCommand);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
|
||||||
return this.installOrUpdateNpmModule(packageName, { version });
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
|
||||||
const communityPackageService = Container.get(CommunityPackageService);
|
|
||||||
|
|
||||||
await communityPackageService.executeNpmCommand(`npm remove ${packageName}`);
|
|
||||||
|
|
||||||
await communityPackageService.removePackageFromDatabase(installedPackage);
|
|
||||||
|
|
||||||
if (packageName in this.loaders) {
|
if (packageName in this.loaders) {
|
||||||
this.loaders[packageName].reset();
|
this.loaders[packageName].reset();
|
||||||
delete this.loaders[packageName];
|
delete this.loaders[packageName];
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.postProcessLoaders();
|
|
||||||
await this.generateTypesForFrontend();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNpmModule(
|
|
||||||
packageName: string,
|
|
||||||
installedPackage: InstalledPackages,
|
|
||||||
): Promise<InstalledPackages> {
|
|
||||||
return this.installOrUpdateNpmModule(packageName, { installedPackage });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -382,5 +296,49 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.injectCustomApiCallOptions();
|
||||||
|
|
||||||
|
for (const postProcessor of this.postProcessors) {
|
||||||
|
await postProcessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupHotReload() {
|
||||||
|
const { default: debounce } = await import('lodash/debounce');
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
const { watch } = await import('chokidar');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { Push } = await import('@/push');
|
||||||
|
const push = Container.get(Push);
|
||||||
|
|
||||||
|
Object.values(this.loaders).forEach(async (loader) => {
|
||||||
|
try {
|
||||||
|
await fsPromises.access(loader.directory);
|
||||||
|
} catch {
|
||||||
|
// If directory doesn't exist, there is nothing to watch
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realModulePath = path.join(await fsPromises.realpath(loader.directory), path.sep);
|
||||||
|
const reloader = debounce(async () => {
|
||||||
|
const modulesToUnload = Object.keys(require.cache).filter((filePath) =>
|
||||||
|
filePath.startsWith(realModulePath),
|
||||||
|
);
|
||||||
|
modulesToUnload.forEach((filePath) => {
|
||||||
|
delete require.cache[filePath];
|
||||||
|
});
|
||||||
|
|
||||||
|
loader.reset();
|
||||||
|
await loader.loadAll();
|
||||||
|
await this.postProcessLoaders();
|
||||||
|
push.send('nodeDescriptionUpdated', undefined);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
const toWatch = loader.isLazyLoaded
|
||||||
|
? ['**/nodes.json', '**/credentials.json']
|
||||||
|
: ['**/*.js', '**/*.json'];
|
||||||
|
watch(toWatch, { cwd: realModulePath }).on('change', reloader);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,8 @@ import type { Dirent } from 'fs';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class NodeTypes implements INodeTypes {
|
export class NodeTypes implements INodeTypes {
|
||||||
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {}
|
constructor(private loadNodesAndCredentials: LoadNodesAndCredentials) {
|
||||||
|
loadNodesAndCredentials.addPostProcessor(async () => this.applySpecialNodeParameters());
|
||||||
init() {
|
|
||||||
// Some nodeTypes need to get special parameters applied like the
|
|
||||||
// polling nodes the polling times
|
|
||||||
this.applySpecialNodeParameters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,20 +46,20 @@ export class NodeTypes implements INodeTypes {
|
||||||
return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version);
|
return NodeHelpers.getVersionedNodeType(this.getNode(nodeType).type, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Some nodeTypes need to get special parameters applied like the polling nodes the polling times */
|
||||||
applySpecialNodeParameters() {
|
applySpecialNodeParameters() {
|
||||||
for (const nodeTypeData of Object.values(this.loadedNodes)) {
|
for (const nodeTypeData of Object.values(this.loadNodesAndCredentials.loadedNodes)) {
|
||||||
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
|
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
|
||||||
NodeHelpers.applySpecialNodeParameters(nodeType);
|
NodeHelpers.applySpecialNodeParameters(nodeType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNode(type: string): LoadedClass<INodeType | IVersionedNodeType> {
|
private getNode(type: string): LoadedClass<INodeType | IVersionedNodeType> {
|
||||||
const loadedNodes = this.loadedNodes;
|
const { loadedNodes, knownNodes } = this.loadNodesAndCredentials;
|
||||||
if (type in loadedNodes) {
|
if (type in loadedNodes) {
|
||||||
return loadedNodes[type];
|
return loadedNodes[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownNodes = this.knownNodes;
|
|
||||||
if (type in knownNodes) {
|
if (type in knownNodes) {
|
||||||
const { className, sourcePath } = knownNodes[type];
|
const { className, sourcePath } = knownNodes[type];
|
||||||
const loaded: INodeType = loadClassInIsolation(sourcePath, className);
|
const loaded: INodeType = loadClassInIsolation(sourcePath, className);
|
||||||
|
@ -74,14 +70,6 @@ export class NodeTypes implements INodeTypes {
|
||||||
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${type}`);
|
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get loadedNodes() {
|
|
||||||
return this.nodesAndCredentials.loaded.nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get knownNodes() {
|
|
||||||
return this.nodesAndCredentials.known.nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNodeTranslationPath({
|
async getNodeTranslationPath({
|
||||||
nodeSourcePath,
|
nodeSourcePath,
|
||||||
longNodeType,
|
longNodeType,
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -11,7 +11,7 @@ import assert from 'assert';
|
||||||
import { exec as callbackExec } from 'child_process';
|
import { exec as callbackExec } from 'child_process';
|
||||||
import { access as fsAccess } from 'fs/promises';
|
import { access as fsAccess } from 'fs/promises';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from 'path';
|
import { join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
import { createHmac } from 'crypto';
|
import { createHmac } from 'crypto';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
|
@ -86,7 +86,6 @@ import {
|
||||||
LdapController,
|
LdapController,
|
||||||
MeController,
|
MeController,
|
||||||
MFAController,
|
MFAController,
|
||||||
NodesController,
|
|
||||||
NodeTypesController,
|
NodeTypesController,
|
||||||
OwnerController,
|
OwnerController,
|
||||||
PasswordResetController,
|
PasswordResetController,
|
||||||
|
@ -172,6 +171,7 @@ import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
|
||||||
import { TOTPService } from './Mfa/totp.service';
|
import { TOTPService } from './Mfa/totp.service';
|
||||||
import { MfaService } from './Mfa/mfa.service';
|
import { MfaService } from './Mfa/mfa.service';
|
||||||
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
|
||||||
|
import type { FrontendService } from './services/frontend.service';
|
||||||
import { JwtService } from './services/jwt.service';
|
import { JwtService } from './services/jwt.service';
|
||||||
import { RoleService } from './services/role.service';
|
import { RoleService } from './services/role.service';
|
||||||
import { UserService } from './services/user.service';
|
import { UserService } from './services/user.service';
|
||||||
|
@ -202,6 +202,8 @@ export class Server extends AbstractServer {
|
||||||
|
|
||||||
credentialTypes: ICredentialTypes;
|
credentialTypes: ICredentialTypes;
|
||||||
|
|
||||||
|
frontendService: FrontendService;
|
||||||
|
|
||||||
postHog: PostHogClient;
|
postHog: PostHogClient;
|
||||||
|
|
||||||
push: Push;
|
push: Push;
|
||||||
|
@ -362,6 +364,16 @@ export class Server extends AbstractServer {
|
||||||
this.credentialTypes = Container.get(CredentialTypes);
|
this.credentialTypes = Container.get(CredentialTypes);
|
||||||
this.nodeTypes = Container.get(NodeTypes);
|
this.nodeTypes = Container.get(NodeTypes);
|
||||||
|
|
||||||
|
if (!config.getEnv('endpoints.disableUi')) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { FrontendService } = await import('@/services/frontend.service');
|
||||||
|
this.frontendService = Container.get(FrontendService);
|
||||||
|
this.loadNodesAndCredentials.addPostProcessor(async () =>
|
||||||
|
this.frontendService.generateTypes(),
|
||||||
|
);
|
||||||
|
await this.frontendService.generateTypes();
|
||||||
|
}
|
||||||
|
|
||||||
this.activeExecutionsInstance = Container.get(ActiveExecutions);
|
this.activeExecutionsInstance = Container.get(ActiveExecutions);
|
||||||
this.waitTracker = Container.get(WaitTracker);
|
this.waitTracker = Container.get(WaitTracker);
|
||||||
this.postHog = Container.get(PostHogClient);
|
this.postHog = Container.get(PostHogClient);
|
||||||
|
@ -419,8 +431,7 @@ export class Server extends AbstractServer {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') {
|
if (inDevelopment && process.env.N8N_DEV_RELOAD === 'true') {
|
||||||
const { reloadNodesAndCredentials } = await import('@/ReloadNodesAndCredentials');
|
void this.loadNodesAndCredentials.setupHotReload();
|
||||||
await reloadNodesAndCredentials(this.loadNodesAndCredentials, this.nodeTypes, this.push);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Db.collections.Workflow.findOne({
|
void Db.collections.Workflow.findOne({
|
||||||
|
@ -435,7 +446,7 @@ export class Server extends AbstractServer {
|
||||||
/**
|
/**
|
||||||
* Returns the current settings for the frontend
|
* Returns the current settings for the frontend
|
||||||
*/
|
*/
|
||||||
getSettingsForFrontend(): IN8nUISettings {
|
private async getSettingsForFrontend(): Promise<IN8nUISettings> {
|
||||||
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
|
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
|
||||||
const instanceBaseUrl = getInstanceBaseUrl();
|
const instanceBaseUrl = getInstanceBaseUrl();
|
||||||
this.frontendSettings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
this.frontendSettings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
||||||
|
@ -506,8 +517,11 @@ export class Server extends AbstractServer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.get('nodes.packagesMissing').length > 0) {
|
if (config.getEnv('nodes.communityPackages.enabled')) {
|
||||||
this.frontendSettings.missingPackages = true;
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
|
||||||
|
this.frontendSettings.missingPackages =
|
||||||
|
Container.get(CommunityPackagesService).hasMissingPackages;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frontendSettings.mfa.enabled = isMfaFeatureEnabled();
|
this.frontendSettings.mfa.enabled = isMfaFeatureEnabled();
|
||||||
|
@ -585,9 +599,11 @@ export class Server extends AbstractServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.getEnv('nodes.communityPackages.enabled')) {
|
if (config.getEnv('nodes.communityPackages.enabled')) {
|
||||||
controllers.push(
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
new NodesController(config, this.loadNodesAndCredentials, this.push, internalHooks),
|
const { CommunityPackagesController } = await import(
|
||||||
|
'@/controllers/communityPackages.controller'
|
||||||
);
|
);
|
||||||
|
controllers.push(Container.get(CommunityPackagesController));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inE2ETests) {
|
if (inE2ETests) {
|
||||||
|
@ -1480,9 +1496,9 @@ export class Server extends AbstractServer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CredentialsOverwrites().setData(body);
|
Container.get(CredentialsOverwrites).setData(body);
|
||||||
|
|
||||||
await this.loadNodesAndCredentials.generateTypesForFrontend();
|
await this.frontendService?.generateTypes();
|
||||||
|
|
||||||
this.presetCredentialsLoaded = true;
|
this.presetCredentialsLoaded = true;
|
||||||
|
|
||||||
|
@ -1509,22 +1525,13 @@ export class Server extends AbstractServer {
|
||||||
const serveIcons: express.RequestHandler = async (req, res) => {
|
const serveIcons: express.RequestHandler = async (req, res) => {
|
||||||
let { scope, packageName } = req.params;
|
let { scope, packageName } = req.params;
|
||||||
if (scope) packageName = `@${scope}/${packageName}`;
|
if (scope) packageName = `@${scope}/${packageName}`;
|
||||||
const loader = this.loadNodesAndCredentials.loaders[packageName];
|
const filePath = this.loadNodesAndCredentials.resolveIcon(packageName, req.originalUrl);
|
||||||
if (loader) {
|
if (filePath) {
|
||||||
const pathPrefix = `/icons/${packageName}/`;
|
|
||||||
const filePath = pathResolve(
|
|
||||||
loader.directory,
|
|
||||||
req.originalUrl.substring(pathPrefix.length),
|
|
||||||
);
|
|
||||||
if (pathRelative(loader.directory, filePath).includes('..')) {
|
|
||||||
return res.status(404).end();
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await fsAccess(filePath);
|
await fsAccess(filePath);
|
||||||
return res.sendFile(filePath);
|
return res.sendFile(filePath);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendStatus(404);
|
res.sendStatus(404);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -35,8 +35,6 @@ import {
|
||||||
WorkflowHooks,
|
WorkflowHooks,
|
||||||
WorkflowOperationError,
|
WorkflowOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import type {
|
import type {
|
||||||
|
@ -115,8 +113,6 @@ class WorkflowRunnerProcess {
|
||||||
await loadNodesAndCredentials.init();
|
await loadNodesAndCredentials.init();
|
||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
const nodeTypes = Container.get(NodeTypes);
|
||||||
const credentialTypes = Container.get(CredentialTypes);
|
|
||||||
CredentialsOverwrites(credentialTypes);
|
|
||||||
|
|
||||||
// Load all external hooks
|
// Load all external hooks
|
||||||
const externalHooks = Container.get(ExternalHooks);
|
const externalHooks = Container.get(ExternalHooks);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
import config from '@/config';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { getNodeTypes } from '@/audit/utils';
|
import { getNodeTypes } from '@/audit/utils';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
|
||||||
import {
|
import {
|
||||||
OFFICIAL_RISKY_NODE_TYPES,
|
OFFICIAL_RISKY_NODE_TYPES,
|
||||||
ENV_VARS_DOCS_URL,
|
ENV_VARS_DOCS_URL,
|
||||||
|
@ -12,10 +13,13 @@ import {
|
||||||
} from '@/audit/constants';
|
} from '@/audit/constants';
|
||||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||||
import type { Risk } from '@/audit/types';
|
import type { Risk } from '@/audit/types';
|
||||||
import { Container } from 'typedi';
|
|
||||||
|
|
||||||
async function getCommunityNodeDetails() {
|
async function getCommunityNodeDetails() {
|
||||||
const installedPackages = await Container.get(CommunityPackageService).getAllInstalledPackages();
|
if (!config.getEnv('nodes.communityPackages.enabled')) return [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
|
||||||
|
const installedPackages = await Container.get(CommunityPackagesService).getAllInstalledPackages();
|
||||||
|
|
||||||
return installedPackages.reduce<Risk.CommunityNodeDetails[]>((acc, pkg) => {
|
return installedPackages.reduce<Risk.CommunityNodeDetails[]>((acc, pkg) => {
|
||||||
pkg.installedNodes.forEach((node) =>
|
pkg.installedNodes.forEach((node) =>
|
||||||
|
|
|
@ -10,8 +10,6 @@ import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import * as CrashJournal from '@/CrashJournal';
|
import * as CrashJournal from '@/CrashJournal';
|
||||||
import { LICENSE_FEATURES, inTest } from '@/constants';
|
import { LICENSE_FEATURES, inTest } from '@/constants';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
|
||||||
import { initErrorHandling } from '@/ErrorReporting';
|
import { initErrorHandling } from '@/ErrorReporting';
|
||||||
import { ExternalHooks } from '@/ExternalHooks';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
|
@ -30,8 +28,6 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
protected externalHooks: IExternalHooksClass;
|
protected externalHooks: IExternalHooksClass;
|
||||||
|
|
||||||
protected loadNodesAndCredentials: LoadNodesAndCredentials;
|
|
||||||
|
|
||||||
protected nodeTypes: NodeTypes;
|
protected nodeTypes: NodeTypes;
|
||||||
|
|
||||||
protected userSettings: IUserSettings;
|
protected userSettings: IUserSettings;
|
||||||
|
@ -54,12 +50,8 @@ export abstract class BaseCommand extends Command {
|
||||||
// Make sure the settings exist
|
// Make sure the settings exist
|
||||||
this.userSettings = await UserSettings.prepareUserSettings();
|
this.userSettings = await UserSettings.prepareUserSettings();
|
||||||
|
|
||||||
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
|
await Container.get(LoadNodesAndCredentials).init();
|
||||||
await this.loadNodesAndCredentials.init();
|
|
||||||
this.nodeTypes = Container.get(NodeTypes);
|
this.nodeTypes = Container.get(NodeTypes);
|
||||||
this.nodeTypes.init();
|
|
||||||
const credentialTypes = Container.get(CredentialTypes);
|
|
||||||
CredentialsOverwrites(credentialTypes);
|
|
||||||
|
|
||||||
await Db.init().catch(async (error: Error) =>
|
await Db.init().catch(async (error: Error) =>
|
||||||
this.exitWithCrash('There was an error initializing DB', error),
|
this.exitWithCrash('There was an error initializing DB', error),
|
||||||
|
|
|
@ -23,7 +23,6 @@ import * as Db from '@/Db';
|
||||||
import * as GenericHelpers from '@/GenericHelpers';
|
import * as GenericHelpers from '@/GenericHelpers';
|
||||||
import { Server } from '@/Server';
|
import { Server } from '@/Server';
|
||||||
import { TestWebhooks } from '@/TestWebhooks';
|
import { TestWebhooks } from '@/TestWebhooks';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
|
||||||
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
|
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
|
||||||
import { eventBus } from '@/eventbus';
|
import { eventBus } from '@/eventbus';
|
||||||
import { BaseCommand } from './BaseCommand';
|
import { BaseCommand } from './BaseCommand';
|
||||||
|
@ -257,8 +256,6 @@ export class Start extends BaseCommand {
|
||||||
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
|
config.set('userManagement.jwtSecret', createHash('sha256').update(baseKey).digest('hex'));
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadNodesAndCredentials.generateTypesForFrontend();
|
|
||||||
|
|
||||||
await UserSettings.getEncryptionKey();
|
await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
// Load settings from database and set them to config.
|
// Load settings from database and set them to config.
|
||||||
|
@ -270,12 +267,11 @@ export class Start extends BaseCommand {
|
||||||
const areCommunityPackagesEnabled = config.getEnv('nodes.communityPackages.enabled');
|
const areCommunityPackagesEnabled = config.getEnv('nodes.communityPackages.enabled');
|
||||||
|
|
||||||
if (areCommunityPackagesEnabled) {
|
if (areCommunityPackagesEnabled) {
|
||||||
await Container.get(CommunityPackageService).setMissingPackages(
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
this.loadNodesAndCredentials,
|
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
|
||||||
{
|
await Container.get(CommunityPackagesService).setMissingPackages({
|
||||||
reinstallMissingPackages: flags.reinstallMissingPackages,
|
reinstallMissingPackages: flags.reinstallMissingPackages,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbType = config.getEnv('database.type');
|
const dbType = config.getEnv('database.type');
|
||||||
|
|
|
@ -463,7 +463,7 @@ export class Worker extends BaseCommand {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CredentialsOverwrites().setData(body);
|
Container.get(CredentialsOverwrites).setData(body);
|
||||||
presetCredentialsLoaded = true;
|
presetCredentialsLoaded = true;
|
||||||
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
|
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -818,12 +818,6 @@ export const schema = {
|
||||||
env: 'N8N_COMMUNITY_PACKAGES_ENABLED',
|
env: 'N8N_COMMUNITY_PACKAGES_ENABLED',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
packagesMissing: {
|
|
||||||
// Used to have a persistent list of packages
|
|
||||||
doc: 'Contains a comma separated list of packages that failed to load during startup',
|
|
||||||
format: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logs: {
|
logs: {
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { Service } from 'typedi';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import config from '@/config';
|
||||||
import {
|
import {
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
STARTER_TEMPLATE_NAME,
|
STARTER_TEMPLATE_NAME,
|
||||||
|
@ -9,12 +11,9 @@ import { NodeRequest } from '@/requests';
|
||||||
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { Config } from '@/config';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
|
||||||
import Container from 'typedi';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PACKAGE_NOT_INSTALLED,
|
PACKAGE_NOT_INSTALLED,
|
||||||
|
@ -33,24 +32,20 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
|
||||||
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
@Authorized(['global', 'owner'])
|
@Authorized(['global', 'owner'])
|
||||||
@RestController('/nodes')
|
@RestController('/community-packages')
|
||||||
export class NodesController {
|
export class CommunityPackagesController {
|
||||||
private communityPackageService: CommunityPackageService;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private config: Config,
|
|
||||||
private loadNodesAndCredentials: LoadNodesAndCredentials,
|
|
||||||
private push: Push,
|
private push: Push,
|
||||||
private internalHooks: InternalHooks,
|
private internalHooks: InternalHooks,
|
||||||
) {
|
private communityPackagesService: CommunityPackagesService,
|
||||||
this.communityPackageService = Container.get(CommunityPackageService);
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
||||||
@Middleware()
|
@Middleware()
|
||||||
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
||||||
if (this.config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
|
if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Package management is disabled when running in "queue" mode',
|
message: 'Package management is disabled when running in "queue" mode',
|
||||||
|
@ -69,7 +64,7 @@ export class NodesController {
|
||||||
let parsed: CommunityPackages.ParsedPackageName;
|
let parsed: CommunityPackages.ParsedPackageName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsed = this.communityPackageService.parseNpmPackageName(name);
|
parsed = this.communityPackagesService.parseNpmPackageName(name);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
error instanceof Error ? error.message : 'Failed to parse package name',
|
error instanceof Error ? error.message : 'Failed to parse package name',
|
||||||
|
@ -85,8 +80,8 @@ export class NodesController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInstalled = await this.communityPackageService.isPackageInstalled(parsed.packageName);
|
const isInstalled = await this.communityPackagesService.isPackageInstalled(parsed.packageName);
|
||||||
const hasLoaded = this.communityPackageService.hasPackageLoaded(name);
|
const hasLoaded = this.communityPackagesService.hasPackageLoaded(name);
|
||||||
|
|
||||||
if (isInstalled && hasLoaded) {
|
if (isInstalled && hasLoaded) {
|
||||||
throw new BadRequestError(
|
throw new BadRequestError(
|
||||||
|
@ -97,7 +92,7 @@ export class NodesController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageStatus = await this.communityPackageService.checkNpmPackageStatus(name);
|
const packageStatus = await this.communityPackagesService.checkNpmPackageStatus(name);
|
||||||
|
|
||||||
if (packageStatus.status !== 'OK') {
|
if (packageStatus.status !== 'OK') {
|
||||||
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`);
|
throw new BadRequestError(`Package "${name}" is banned so it cannot be installed`);
|
||||||
|
@ -105,7 +100,7 @@ export class NodesController {
|
||||||
|
|
||||||
let installedPackage: InstalledPackages;
|
let installedPackage: InstalledPackages;
|
||||||
try {
|
try {
|
||||||
installedPackage = await this.loadNodesAndCredentials.installNpmModule(
|
installedPackage = await this.communityPackagesService.installNpmModule(
|
||||||
parsed.packageName,
|
parsed.packageName,
|
||||||
parsed.version,
|
parsed.version,
|
||||||
);
|
);
|
||||||
|
@ -130,7 +125,7 @@ export class NodesController {
|
||||||
throw new (clientError ? BadRequestError : InternalServerError)(message);
|
throw new (clientError ? BadRequestError : InternalServerError)(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasLoaded) this.communityPackageService.removePackageFromMissingList(name);
|
if (!hasLoaded) this.communityPackagesService.removePackageFromMissingList(name);
|
||||||
|
|
||||||
// broadcast to connected frontends that node list has been updated
|
// broadcast to connected frontends that node list has been updated
|
||||||
installedPackage.installedNodes.forEach((node) => {
|
installedPackage.installedNodes.forEach((node) => {
|
||||||
|
@ -156,7 +151,7 @@ export class NodesController {
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getInstalledPackages() {
|
async getInstalledPackages() {
|
||||||
const installedPackages = await this.communityPackageService.getAllInstalledPackages();
|
const installedPackages = await this.communityPackagesService.getAllInstalledPackages();
|
||||||
|
|
||||||
if (installedPackages.length === 0) return [];
|
if (installedPackages.length === 0) return [];
|
||||||
|
|
||||||
|
@ -164,7 +159,7 @@ export class NodesController {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const command = ['npm', 'outdated', '--json'].join(' ');
|
const command = ['npm', 'outdated', '--json'].join(' ');
|
||||||
await this.communityPackageService.executeNpmCommand(command, { doNotHandleError: true });
|
await this.communityPackagesService.executeNpmCommand(command, { doNotHandleError: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// when there are updates, npm exits with code 1
|
// when there are updates, npm exits with code 1
|
||||||
// when there are no updates, command succeeds
|
// when there are no updates, command succeeds
|
||||||
|
@ -174,18 +169,14 @@ export class NodesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hydratedPackages = this.communityPackageService.matchPackagesWithUpdates(
|
let hydratedPackages = this.communityPackagesService.matchPackagesWithUpdates(
|
||||||
installedPackages,
|
installedPackages,
|
||||||
pendingUpdates,
|
pendingUpdates,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const missingPackages = this.config.get('nodes.packagesMissing') as string | undefined;
|
if (this.communityPackagesService.hasMissingPackages) {
|
||||||
if (missingPackages) {
|
hydratedPackages = this.communityPackagesService.matchMissingPackages(hydratedPackages);
|
||||||
hydratedPackages = this.communityPackageService.matchMissingPackages(
|
|
||||||
hydratedPackages,
|
|
||||||
missingPackages,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
@ -201,21 +192,21 @@ export class NodesController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.communityPackageService.parseNpmPackageName(name); // sanitize input
|
this.communityPackagesService.parseNpmPackageName(name); // sanitize input
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||||
|
|
||||||
throw new BadRequestError(message);
|
throw new BadRequestError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackage = await this.communityPackageService.findInstalledPackage(name);
|
const installedPackage = await this.communityPackagesService.findInstalledPackage(name);
|
||||||
|
|
||||||
if (!installedPackage) {
|
if (!installedPackage) {
|
||||||
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.loadNodesAndCredentials.removeNpmModule(name, installedPackage);
|
await this.communityPackagesService.removeNpmModule(name, installedPackage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = [
|
const message = [
|
||||||
`Error removing package "${name}"`,
|
`Error removing package "${name}"`,
|
||||||
|
@ -252,15 +243,15 @@ export class NodesController {
|
||||||
}
|
}
|
||||||
|
|
||||||
const previouslyInstalledPackage =
|
const previouslyInstalledPackage =
|
||||||
await this.communityPackageService.findInstalledPackage(name);
|
await this.communityPackagesService.findInstalledPackage(name);
|
||||||
|
|
||||||
if (!previouslyInstalledPackage) {
|
if (!previouslyInstalledPackage) {
|
||||||
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
throw new BadRequestError(PACKAGE_NOT_INSTALLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newInstalledPackage = await this.loadNodesAndCredentials.updateNpmModule(
|
const newInstalledPackage = await this.communityPackagesService.updateNpmModule(
|
||||||
this.communityPackageService.parseNpmPackageName(name).packageName,
|
this.communityPackagesService.parseNpmPackageName(name).packageName,
|
||||||
previouslyInstalledPackage,
|
previouslyInstalledPackage,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,6 @@ export { AuthController } from './auth.controller';
|
||||||
export { LdapController } from './ldap.controller';
|
export { LdapController } from './ldap.controller';
|
||||||
export { MeController } from './me.controller';
|
export { MeController } from './me.controller';
|
||||||
export { MFAController } from './mfa.controller';
|
export { MFAController } from './mfa.controller';
|
||||||
export { NodesController } from './nodes.controller';
|
|
||||||
export { NodeTypesController } from './nodeTypes.controller';
|
export { NodeTypesController } from './nodeTypes.controller';
|
||||||
export { OwnerController } from './owner.controller';
|
export { OwnerController } from './owner.controller';
|
||||||
export { PasswordResetController } from './passwordReset.controller';
|
export { PasswordResetController } from './passwordReset.controller';
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||||
|
|
||||||
import { LoggerProxy as Logger } from 'n8n-workflow';
|
|
||||||
import { UserSettings } from 'n8n-core';
|
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import config from '@/config';
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
import { UserSettings } from 'n8n-core';
|
||||||
|
import type { PackageDirectoryLoader } from 'n8n-core';
|
||||||
|
|
||||||
import { toError } from '@/utils';
|
import { toError } from '@/utils';
|
||||||
import { InstalledPackagesRepository } from '@/databases/repositories/installedPackages.repository';
|
import { InstalledPackagesRepository } from '@/databases/repositories/installedPackages.repository';
|
||||||
import type { InstalledPackages } from '@/databases/entities/InstalledPackages';
|
import type { InstalledPackages } from '@/databases/entities/InstalledPackages';
|
||||||
|
@ -18,11 +19,8 @@ import {
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
UNKNOWN_FAILURE_REASON,
|
UNKNOWN_FAILURE_REASON,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
|
||||||
import type { PackageDirectoryLoader } from 'n8n-core';
|
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PACKAGE_NAME_NOT_PROVIDED,
|
PACKAGE_NAME_NOT_PROVIDED,
|
||||||
|
@ -45,8 +43,17 @@ const asyncExec = promisify(exec);
|
||||||
const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
|
const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommunityPackageService {
|
export class CommunityPackagesService {
|
||||||
constructor(private readonly installedPackageRepository: InstalledPackagesRepository) {}
|
missingPackages: string[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly installedPackageRepository: InstalledPackagesRepository,
|
||||||
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get hasMissingPackages() {
|
||||||
|
return this.missingPackages.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
async findInstalledPackage(packageName: string) {
|
async findInstalledPackage(packageName: string) {
|
||||||
return this.installedPackageRepository.findOne({
|
return this.installedPackageRepository.findOne({
|
||||||
|
@ -173,9 +180,8 @@ export class CommunityPackageService {
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
matchMissingPackages(installedPackages: PublicInstalledPackage[], missingPackages: string) {
|
matchMissingPackages(installedPackages: PublicInstalledPackage[]) {
|
||||||
const missingPackagesList = missingPackages
|
const missingPackagesList = this.missingPackages
|
||||||
.split(' ')
|
|
||||||
.map((name) => {
|
.map((name) => {
|
||||||
try {
|
try {
|
||||||
// Strip away versions but maintain scope and package name
|
// Strip away versions but maintain scope and package name
|
||||||
|
@ -221,13 +227,9 @@ export class CommunityPackageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPackageLoaded(packageName: string) {
|
hasPackageLoaded(packageName: string) {
|
||||||
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
if (!this.missingPackages.length) return true;
|
||||||
|
|
||||||
if (!missingPackages) return true;
|
return !this.missingPackages.some(
|
||||||
|
|
||||||
return !missingPackages
|
|
||||||
.split(' ')
|
|
||||||
.some(
|
|
||||||
(packageNameAndVersion) =>
|
(packageNameAndVersion) =>
|
||||||
packageNameAndVersion.startsWith(packageName) &&
|
packageNameAndVersion.startsWith(packageName) &&
|
||||||
packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
||||||
|
@ -236,30 +238,23 @@ export class CommunityPackageService {
|
||||||
|
|
||||||
removePackageFromMissingList(packageName: string) {
|
removePackageFromMissingList(packageName: string) {
|
||||||
try {
|
try {
|
||||||
const failedPackages = config.get('nodes.packagesMissing').split(' ');
|
this.missingPackages = this.missingPackages.filter(
|
||||||
|
|
||||||
const packageFailedToLoad = failedPackages.filter(
|
|
||||||
(packageNameAndVersion) =>
|
(packageNameAndVersion) =>
|
||||||
!packageNameAndVersion.startsWith(packageName) ||
|
!packageNameAndVersion.startsWith(packageName) ||
|
||||||
!packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
!packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
||||||
);
|
);
|
||||||
|
|
||||||
config.set('nodes.packagesMissing', packageFailedToLoad.join(' '));
|
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMissingPackages(
|
async setMissingPackages({ reinstallMissingPackages }: { reinstallMissingPackages: boolean }) {
|
||||||
loadNodesAndCredentials: LoadNodesAndCredentials,
|
|
||||||
{ reinstallMissingPackages }: { reinstallMissingPackages: boolean },
|
|
||||||
) {
|
|
||||||
const installedPackages = await this.getAllInstalledPackages();
|
const installedPackages = await this.getAllInstalledPackages();
|
||||||
const missingPackages = new Set<{ packageName: string; version: string }>();
|
const missingPackages = new Set<{ packageName: string; version: string }>();
|
||||||
|
|
||||||
installedPackages.forEach((installedPackage) => {
|
installedPackages.forEach((installedPackage) => {
|
||||||
installedPackage.installedNodes.forEach((installedNode) => {
|
installedPackage.installedNodes.forEach((installedNode) => {
|
||||||
if (!loadNodesAndCredentials.known.nodes[installedNode.type]) {
|
if (!this.loadNodesAndCredentials.isKnownNode(installedNode.type)) {
|
||||||
// Leave the list ready for installing in case we need.
|
// Leave the list ready for installing in case we need.
|
||||||
missingPackages.add({
|
missingPackages.add({
|
||||||
packageName: installedPackage.packageName,
|
packageName: installedPackage.packageName,
|
||||||
|
@ -269,7 +264,7 @@ export class CommunityPackageService {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
config.set('nodes.packagesMissing', '');
|
this.missingPackages = [];
|
||||||
|
|
||||||
if (missingPackages.size === 0) return;
|
if (missingPackages.size === 0) return;
|
||||||
|
|
||||||
|
@ -283,10 +278,7 @@ export class CommunityPackageService {
|
||||||
// Optimistic approach - stop if any installation fails
|
// Optimistic approach - stop if any installation fails
|
||||||
|
|
||||||
for (const missingPackage of missingPackages) {
|
for (const missingPackage of missingPackages) {
|
||||||
await loadNodesAndCredentials.installNpmModule(
|
await this.installNpmModule(missingPackage.packageName, missingPackage.version);
|
||||||
missingPackage.packageName,
|
|
||||||
missingPackage.version,
|
|
||||||
);
|
|
||||||
|
|
||||||
missingPackages.delete(missingPackage);
|
missingPackages.delete(missingPackage);
|
||||||
}
|
}
|
||||||
|
@ -296,11 +288,79 @@ export class CommunityPackageService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.set(
|
this.missingPackages = [...missingPackages].map(
|
||||||
'nodes.packagesMissing',
|
(missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`,
|
||||||
Array.from(missingPackages)
|
|
||||||
.map((missingPackage) => `${missingPackage.packageName}@${missingPackage.version}`)
|
|
||||||
.join(' '),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
||||||
|
return this.installOrUpdateNpmModule(packageName, { version });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNpmModule(
|
||||||
|
packageName: string,
|
||||||
|
installedPackage: InstalledPackages,
|
||||||
|
): Promise<InstalledPackages> {
|
||||||
|
return this.installOrUpdateNpmModule(packageName, { installedPackage });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
||||||
|
await this.executeNpmCommand(`npm remove ${packageName}`);
|
||||||
|
await this.removePackageFromDatabase(installedPackage);
|
||||||
|
await this.loadNodesAndCredentials.unloadPackage(packageName);
|
||||||
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async installOrUpdateNpmModule(
|
||||||
|
packageName: string,
|
||||||
|
options: { version?: string } | { installedPackage: InstalledPackages },
|
||||||
|
) {
|
||||||
|
const isUpdate = 'installedPackage' in options;
|
||||||
|
const command = isUpdate
|
||||||
|
? `npm update ${packageName}`
|
||||||
|
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.executeNpmCommand(command);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
||||||
|
throw new Error(`The npm package "${packageName}" could not be found.`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let loader: PackageDirectoryLoader;
|
||||||
|
try {
|
||||||
|
loader = await this.loadNodesAndCredentials.loadPackage(packageName);
|
||||||
|
} catch (error) {
|
||||||
|
// Remove this package since loading it failed
|
||||||
|
const removeCommand = `npm remove ${packageName}`;
|
||||||
|
try {
|
||||||
|
await this.executeNpmCommand(removeCommand);
|
||||||
|
} catch {}
|
||||||
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loader.loadedNodes.length > 0) {
|
||||||
|
// Save info to DB
|
||||||
|
try {
|
||||||
|
if (isUpdate) {
|
||||||
|
await this.removePackageFromDatabase(options.installedPackage);
|
||||||
|
}
|
||||||
|
const installedPackage = await this.persistInstalledPackage(loader);
|
||||||
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
|
return installedPackage;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to save installed package: ${packageName}`, { cause: error });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove this package since it contains no loadable nodes
|
||||||
|
const removeCommand = `npm remove ${packageName}`;
|
||||||
|
try {
|
||||||
|
await this.executeNpmCommand(removeCommand);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
67
packages/cli/src/services/frontend.service.ts
Normal file
67
packages/cli/src/services/frontend.service.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
import uniq from 'lodash/uniq';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
import { mkdir } from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import type { ICredentialType, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { GENERATED_STATIC_DIR } from '@/constants';
|
||||||
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class FrontendService {
|
||||||
|
constructor(
|
||||||
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
|
private readonly credentialTypes: CredentialTypes,
|
||||||
|
private readonly credentialsOverwrites: CredentialsOverwrites,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async generateTypes() {
|
||||||
|
this.overwriteCredentialsProperties();
|
||||||
|
|
||||||
|
// pre-render all the node and credential types as static json files
|
||||||
|
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true });
|
||||||
|
const { credentials, nodes } = this.loadNodesAndCredentials.types;
|
||||||
|
this.writeStaticJSON('nodes', nodes);
|
||||||
|
this.writeStaticJSON('credentials', credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) {
|
||||||
|
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`);
|
||||||
|
const stream = createWriteStream(filePath, 'utf-8');
|
||||||
|
stream.write('[\n');
|
||||||
|
data.forEach((entry, index) => {
|
||||||
|
stream.write(JSON.stringify(entry));
|
||||||
|
if (index !== data.length - 1) stream.write(',');
|
||||||
|
stream.write('\n');
|
||||||
|
});
|
||||||
|
stream.write(']\n');
|
||||||
|
stream.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
private overwriteCredentialsProperties() {
|
||||||
|
const { credentials } = this.loadNodesAndCredentials.types;
|
||||||
|
const credentialsOverwrites = this.credentialsOverwrites.getAll();
|
||||||
|
for (const credential of credentials) {
|
||||||
|
const overwrittenProperties = [];
|
||||||
|
this.credentialTypes
|
||||||
|
.getParentTypes(credential.name)
|
||||||
|
.reverse()
|
||||||
|
.map((name) => credentialsOverwrites[name])
|
||||||
|
.forEach((overwrite) => {
|
||||||
|
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (credential.name in credentialsOverwrites) {
|
||||||
|
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overwrittenProperties.length) {
|
||||||
|
credential.__overwrittenProperties = uniq(overwrittenProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import { toReportTitle } from '@/audit/utils';
|
||||||
import { mockInstance } from '../shared/utils/';
|
import { mockInstance } from '../shared/utils/';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
@ -19,8 +19,8 @@ LoggerProxy.init(getLogger());
|
||||||
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
||||||
nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
|
nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
|
||||||
mockInstance(NodeTypes);
|
mockInstance(NodeTypes);
|
||||||
const communityPackageService = mockInstance(CommunityPackageService);
|
const communityPackagesService = mockInstance(CommunityPackagesService);
|
||||||
Container.set(CommunityPackageService, communityPackageService);
|
Container.set(CommunityPackagesService, communityPackagesService);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await testDb.init();
|
await testDb.init();
|
||||||
|
@ -36,7 +36,7 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report risky official nodes', async () => {
|
test('should report risky official nodes', async () => {
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||||
const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => {
|
const map = [...OFFICIAL_RISKY_NODE_TYPES].reduce<{ [nodeType: string]: string }>((acc, cur) => {
|
||||||
return (acc[cur] = uuid()), acc;
|
return (acc[cur] = uuid()), acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -81,7 +81,7 @@ test('should report risky official nodes', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not report non-risky official nodes', async () => {
|
test('should not report non-risky official nodes', async () => {
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||||
await saveManualTriggerWorkflow();
|
await saveManualTriggerWorkflow();
|
||||||
|
|
||||||
const testAudit = await audit(['nodes']);
|
const testAudit = await audit(['nodes']);
|
||||||
|
@ -96,7 +96,7 @@ test('should not report non-risky official nodes', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report community nodes', async () => {
|
test('should report community nodes', async () => {
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue(MOCK_PACKAGE);
|
||||||
|
|
||||||
const testAudit = await audit(['nodes']);
|
const testAudit = await audit(['nodes']);
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import * as testDb from '../shared/testDb';
|
import * as Config from '@oclif/config';
|
||||||
import { mockInstance } from '../shared/utils/';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { type ILogger, LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { ImportWorkflowsCommand } from '@/commands/import/workflow';
|
import { ImportWorkflowsCommand } from '@/commands/import/workflow';
|
||||||
import * as Config from '@oclif/config';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
import * as testDb from '../shared/testDb';
|
||||||
|
import { mockInstance } from '../shared/utils/';
|
||||||
|
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
LoggerProxy.init(mock<ILogger>());
|
||||||
import { getLogger } from '@/Logger';
|
|
||||||
|
|
||||||
LoggerProxy.init(getLogger());
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mockInstance(InternalHooks);
|
mockInstance(InternalHooks);
|
||||||
|
mockInstance(LoadNodesAndCredentials);
|
||||||
await testDb.init();
|
await testDb.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import type { SuperAgentTest } from 'supertest';
|
||||||
|
|
||||||
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
|
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
|
|
||||||
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
|
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
|
||||||
import * as testDb from './shared/testDb';
|
import * as testDb from './shared/testDb';
|
||||||
|
@ -14,15 +17,13 @@ import {
|
||||||
mockPackageName,
|
mockPackageName,
|
||||||
} from './shared/utils';
|
} from './shared/utils';
|
||||||
|
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
const communityPackagesService = mockInstance(CommunityPackagesService, {
|
||||||
import type { InstalledNodes } from '@db/entities/InstalledNodes';
|
hasMissingPackages: false,
|
||||||
import type { SuperAgentTest } from 'supertest';
|
});
|
||||||
|
mockInstance(LoadNodesAndCredentials);
|
||||||
const communityPackageService = mockInstance(CommunityPackageService);
|
|
||||||
const mockLoadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
|
||||||
mockInstance(Push);
|
mockInstance(Push);
|
||||||
|
|
||||||
const testServer = setupTestServer({ endpointGroups: ['nodes'] });
|
const testServer = setupTestServer({ endpointGroups: ['community-packages'] });
|
||||||
|
|
||||||
const commonUpdatesProps = {
|
const commonUpdatesProps = {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
@ -47,12 +48,12 @@ beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /nodes', () => {
|
describe('GET /community-packages', () => {
|
||||||
test('should respond 200 if no nodes are installed', async () => {
|
test('should respond 200 if no nodes are installed', async () => {
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([]);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue([]);
|
||||||
const {
|
const {
|
||||||
body: { data },
|
body: { data },
|
||||||
} = await authAgent.get('/nodes').expect(200);
|
} = await authAgent.get('/community-packages').expect(200);
|
||||||
|
|
||||||
expect(data).toHaveLength(0);
|
expect(data).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
@ -61,12 +62,12 @@ describe('GET /nodes', () => {
|
||||||
const pkg = mockPackage();
|
const pkg = mockPackage();
|
||||||
const node = mockNode(pkg.packageName);
|
const node = mockNode(pkg.packageName);
|
||||||
pkg.installedNodes = [node];
|
pkg.installedNodes = [node];
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
||||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([pkg]);
|
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([pkg]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: { data },
|
body: { data },
|
||||||
} = await authAgent.get('/nodes').expect(200);
|
} = await authAgent.get('/community-packages').expect(200);
|
||||||
|
|
||||||
expect(data).toHaveLength(1);
|
expect(data).toHaveLength(1);
|
||||||
expect(data[0].installedNodes).toHaveLength(1);
|
expect(data[0].installedNodes).toHaveLength(1);
|
||||||
|
@ -80,9 +81,9 @@ describe('GET /nodes', () => {
|
||||||
const nodeB = mockNode(pkgB.packageName);
|
const nodeB = mockNode(pkgB.packageName);
|
||||||
const nodeC = mockNode(pkgB.packageName);
|
const nodeC = mockNode(pkgB.packageName);
|
||||||
|
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkgA, pkgB]);
|
||||||
|
|
||||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
|
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
|
||||||
{
|
{
|
||||||
...commonUpdatesProps,
|
...commonUpdatesProps,
|
||||||
packageName: pkgA.packageName,
|
packageName: pkgA.packageName,
|
||||||
|
@ -97,7 +98,7 @@ describe('GET /nodes', () => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: { data },
|
body: { data },
|
||||||
} = await authAgent.get('/nodes').expect(200);
|
} = await authAgent.get('/community-packages').expect(200);
|
||||||
|
|
||||||
expect(data).toHaveLength(2);
|
expect(data).toHaveLength(2);
|
||||||
|
|
||||||
|
@ -110,26 +111,26 @@ describe('GET /nodes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not check for updates if no packages installed', async () => {
|
test('should not check for updates if no packages installed', async () => {
|
||||||
await authAgent.get('/nodes');
|
await authAgent.get('/community-packages');
|
||||||
|
|
||||||
expect(communityPackageService.executeNpmCommand).not.toHaveBeenCalled();
|
expect(communityPackagesService.executeNpmCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should check for updates if packages installed', async () => {
|
test('should check for updates if packages installed', async () => {
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue([mockPackage()]);
|
||||||
|
|
||||||
await authAgent.get('/nodes').expect(200);
|
await authAgent.get('/community-packages').expect(200);
|
||||||
|
|
||||||
const args = ['npm outdated --json', { doNotHandleError: true }];
|
const args = ['npm outdated --json', { doNotHandleError: true }];
|
||||||
|
|
||||||
expect(communityPackageService.executeNpmCommand).toHaveBeenCalledWith(...args);
|
expect(communityPackagesService.executeNpmCommand).toHaveBeenCalledWith(...args);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should report package updates if available', async () => {
|
test('should report package updates if available', async () => {
|
||||||
const pkg = mockPackage();
|
const pkg = mockPackage();
|
||||||
communityPackageService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
communityPackagesService.getAllInstalledPackages.mockResolvedValue([pkg]);
|
||||||
|
|
||||||
communityPackageService.executeNpmCommand.mockImplementation(() => {
|
communityPackagesService.executeNpmCommand.mockImplementation(() => {
|
||||||
throw {
|
throw {
|
||||||
code: 1,
|
code: 1,
|
||||||
stdout: JSON.stringify({
|
stdout: JSON.stringify({
|
||||||
|
@ -143,7 +144,7 @@ describe('GET /nodes', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
communityPackageService.matchPackagesWithUpdates.mockReturnValue([
|
communityPackagesService.matchPackagesWithUpdates.mockReturnValue([
|
||||||
{
|
{
|
||||||
packageName: 'test',
|
packageName: 'test',
|
||||||
installedNodes: [],
|
installedNodes: [],
|
||||||
|
@ -153,7 +154,7 @@ describe('GET /nodes', () => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: { data },
|
body: { data },
|
||||||
} = await authAgent.get('/nodes').expect(200);
|
} = await authAgent.get('/community-packages').expect(200);
|
||||||
|
|
||||||
const [returnedPkg] = data;
|
const [returnedPkg] = data;
|
||||||
|
|
||||||
|
@ -162,89 +163,92 @@ describe('GET /nodes', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /nodes', () => {
|
describe('POST /community-packages', () => {
|
||||||
test('should reject if package name is missing', async () => {
|
test('should reject if package name is missing', async () => {
|
||||||
await authAgent.post('/nodes').expect(400);
|
await authAgent.post('/community-packages').expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject if package is duplicate', async () => {
|
test('should reject if package is duplicate', async () => {
|
||||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||||
communityPackageService.isPackageInstalled.mockResolvedValue(true);
|
communityPackagesService.isPackageInstalled.mockResolvedValue(true);
|
||||||
communityPackageService.hasPackageLoaded.mockReturnValue(true);
|
communityPackagesService.hasPackageLoaded.mockReturnValue(true);
|
||||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: { message },
|
body: { message },
|
||||||
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
|
} = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
|
||||||
|
|
||||||
expect(message).toContain('already installed');
|
expect(message).toContain('already installed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow installing packages that could not be loaded', async () => {
|
test('should allow installing packages that could not be loaded', async () => {
|
||||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||||
communityPackageService.hasPackageLoaded.mockReturnValue(false);
|
communityPackagesService.hasPackageLoaded.mockReturnValue(false);
|
||||||
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
|
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
|
||||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||||
mockLoadNodesAndCredentials.installNpmModule.mockResolvedValue(mockPackage());
|
communityPackagesService.installNpmModule.mockResolvedValue(mockPackage());
|
||||||
|
|
||||||
await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(200);
|
await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200);
|
||||||
|
|
||||||
expect(communityPackageService.removePackageFromMissingList).toHaveBeenCalled();
|
expect(communityPackagesService.removePackageFromMissingList).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not install a banned package', async () => {
|
test('should not install a banned package', async () => {
|
||||||
communityPackageService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
|
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'Banned' });
|
||||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
body: { message },
|
body: { message },
|
||||||
} = await authAgent.post('/nodes').send({ name: mockPackageName() }).expect(400);
|
} = await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(400);
|
||||||
|
|
||||||
expect(message).toContain('banned');
|
expect(message).toContain('banned');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /nodes', () => {
|
describe('DELETE /community-packages', () => {
|
||||||
test('should not delete if package name is empty', async () => {
|
test('should not delete if package name is empty', async () => {
|
||||||
await authAgent.delete('/nodes').expect(400);
|
await authAgent.delete('/community-packages').expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject if package is not installed', async () => {
|
test('should reject if package is not installed', async () => {
|
||||||
const {
|
const {
|
||||||
body: { message },
|
body: { message },
|
||||||
} = await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(400);
|
} = await authAgent
|
||||||
|
.delete('/community-packages')
|
||||||
|
.query({ name: mockPackageName() })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
expect(message).toContain('not installed');
|
expect(message).toContain('not installed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should uninstall package', async () => {
|
test('should uninstall package', async () => {
|
||||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||||
|
|
||||||
await authAgent.delete('/nodes').query({ name: mockPackageName() }).expect(200);
|
await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200);
|
||||||
|
|
||||||
expect(mockLoadNodesAndCredentials.removeNpmModule).toHaveBeenCalledTimes(1);
|
expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /nodes', () => {
|
describe('PATCH /community-packages', () => {
|
||||||
test('should reject if package name is empty', async () => {
|
test('should reject if package name is empty', async () => {
|
||||||
await authAgent.patch('/nodes').expect(400);
|
await authAgent.patch('/community-packages').expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject if package is not installed', async () => {
|
test('should reject if package is not installed', async () => {
|
||||||
const {
|
const {
|
||||||
body: { message },
|
body: { message },
|
||||||
} = await authAgent.patch('/nodes').send({ name: mockPackageName() }).expect(400);
|
} = await authAgent.patch('/community-packages').send({ name: mockPackageName() }).expect(400);
|
||||||
|
|
||||||
expect(message).toContain('not installed');
|
expect(message).toContain('not installed');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update a package', async () => {
|
test('should update a package', async () => {
|
||||||
communityPackageService.findInstalledPackage.mockResolvedValue(mockPackage());
|
communityPackagesService.findInstalledPackage.mockResolvedValue(mockPackage());
|
||||||
communityPackageService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||||
|
|
||||||
await authAgent.patch('/nodes').send({ name: mockPackageName() });
|
await authAgent.patch('/community-packages').send({ name: mockPackageName() });
|
||||||
|
|
||||||
expect(mockLoadNodesAndCredentials.updateNpmModule).toHaveBeenCalledTimes(1);
|
expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -22,7 +22,7 @@ export type EndpointGroup =
|
||||||
| 'credentials'
|
| 'credentials'
|
||||||
| 'workflows'
|
| 'workflows'
|
||||||
| 'publicApi'
|
| 'publicApi'
|
||||||
| 'nodes'
|
| 'community-packages'
|
||||||
| 'ldap'
|
| 'ldap'
|
||||||
| 'saml'
|
| 'saml'
|
||||||
| 'sourceControl'
|
| 'sourceControl'
|
||||||
|
|
|
@ -25,7 +25,6 @@ import {
|
||||||
LdapController,
|
LdapController,
|
||||||
MFAController,
|
MFAController,
|
||||||
MeController,
|
MeController,
|
||||||
NodesController,
|
|
||||||
OwnerController,
|
OwnerController,
|
||||||
PasswordResetController,
|
PasswordResetController,
|
||||||
TagsController,
|
TagsController,
|
||||||
|
@ -34,12 +33,10 @@ import {
|
||||||
import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
|
import { rawBodyReader, bodyParser, setupAuthMiddlewares } from '@/middlewares';
|
||||||
|
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
|
||||||
import { PostHogClient } from '@/posthog';
|
import { PostHogClient } from '@/posthog';
|
||||||
import { variablesController } from '@/environments/variables/variables.controller';
|
import { variablesController } from '@/environments/variables/variables.controller';
|
||||||
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
import { LdapManager } from '@/Ldap/LdapManager.ee';
|
||||||
import { handleLdapInit } from '@/Ldap/helpers';
|
import { handleLdapInit } from '@/Ldap/helpers';
|
||||||
import { Push } from '@/push';
|
|
||||||
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
|
import { setSamlLoginEnabled } from '@/sso/saml/samlHelpers';
|
||||||
import { SamlController } from '@/sso/saml/routes/saml.controller.ee';
|
import { SamlController } from '@/sso/saml/routes/saml.controller.ee';
|
||||||
import { EventBusController } from '@/eventbus/eventBus.controller';
|
import { EventBusController } from '@/eventbus/eventBus.controller';
|
||||||
|
@ -242,17 +239,11 @@ export const setupTestServer = ({
|
||||||
case 'sourceControl':
|
case 'sourceControl':
|
||||||
registerController(app, config, Container.get(SourceControlController));
|
registerController(app, config, Container.get(SourceControlController));
|
||||||
break;
|
break;
|
||||||
case 'nodes':
|
case 'community-packages':
|
||||||
registerController(
|
const { CommunityPackagesController } = await import(
|
||||||
app,
|
'@/controllers/communityPackages.controller'
|
||||||
config,
|
|
||||||
new NodesController(
|
|
||||||
config,
|
|
||||||
Container.get(LoadNodesAndCredentials),
|
|
||||||
Container.get(Push),
|
|
||||||
internalHooks,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
registerController(app, config, Container.get(CommunityPackagesController));
|
||||||
case 'me':
|
case 'me':
|
||||||
registerController(
|
registerController(
|
||||||
app,
|
app,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import type { ICredentialTypes, INode, INodesAndCredentials } from 'n8n-workflow';
|
import type { INode } from 'n8n-workflow';
|
||||||
import { LoggerProxy, NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow';
|
import { LoggerProxy, NodeApiError, NodeOperationError, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
|
||||||
|
@ -11,22 +12,19 @@ import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||||
import { Role } from '@db/entities/Role';
|
import { Role } from '@db/entities/Role';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
import { getLogger } from '@/Logger';
|
import { getLogger } from '@/Logger';
|
||||||
import { randomEmail, randomName } from '../integration/shared/random';
|
|
||||||
import * as Helpers from './Helpers';
|
|
||||||
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
|
||||||
|
|
||||||
import { WorkflowRunner } from '@/WorkflowRunner';
|
import { WorkflowRunner } from '@/WorkflowRunner';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { ExternalHooks } from '@/ExternalHooks';
|
||||||
import type { ExternalHooks } from '@/ExternalHooks';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { mockInstance } from '../integration/shared/utils/';
|
|
||||||
import { Push } from '@/push';
|
import { Push } from '@/push';
|
||||||
import { ActiveExecutions } from '@/ActiveExecutions';
|
import { ActiveExecutions } from '@/ActiveExecutions';
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
|
||||||
import { SecretsHelper } from '@/SecretsHelpers';
|
import { SecretsHelper } from '@/SecretsHelpers';
|
||||||
import { WebhookService } from '@/services/webhook.service';
|
import { WebhookService } from '@/services/webhook.service';
|
||||||
import { VariablesService } from '../../src/environments/variables/variables.service';
|
import { VariablesService } from '@/environments/variables/variables.service';
|
||||||
|
|
||||||
|
import { mockInstance } from '../integration/shared/utils/';
|
||||||
|
import { randomEmail, randomName } from '../integration/shared/random';
|
||||||
|
import * as Helpers from './Helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
|
@ -114,13 +112,6 @@ jest.mock('@/Db', () => {
|
||||||
return fakeQueryBuilder;
|
return fakeQueryBuilder;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
Webhook: {
|
|
||||||
clear: jest.fn(),
|
|
||||||
delete: jest.fn(),
|
|
||||||
},
|
|
||||||
Variables: {
|
|
||||||
find: jest.fn(() => []),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -140,37 +131,24 @@ const workflowExecuteAdditionalDataExecuteErrorWorkflowSpy = jest.spyOn(
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('ActiveWorkflowRunner', () => {
|
describe('ActiveWorkflowRunner', () => {
|
||||||
let externalHooks: ExternalHooks;
|
mockInstance(ActiveExecutions);
|
||||||
let activeWorkflowRunner: ActiveWorkflowRunner;
|
const externalHooks = mockInstance(ExternalHooks);
|
||||||
const webhookService = mockInstance(WebhookService);
|
const webhookService = mockInstance(WebhookService);
|
||||||
|
mockInstance(Push);
|
||||||
|
mockInstance(SecretsHelper);
|
||||||
|
const variablesService = mockInstance(VariablesService);
|
||||||
|
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
||||||
|
Object.assign(nodesAndCredentials, {
|
||||||
|
loadedNodes: MOCK_NODE_TYPES_DATA,
|
||||||
|
known: { nodes: {}, credentials: {} },
|
||||||
|
types: { nodes: [], credentials: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
LoggerProxy.init(getLogger());
|
LoggerProxy.init(getLogger());
|
||||||
const nodesAndCredentials: INodesAndCredentials = {
|
variablesService.getAllCached.mockResolvedValue([]);
|
||||||
loaded: {
|
|
||||||
nodes: MOCK_NODE_TYPES_DATA,
|
|
||||||
credentials: {},
|
|
||||||
},
|
|
||||||
known: { nodes: {}, credentials: {} },
|
|
||||||
credentialTypes: {} as ICredentialTypes,
|
|
||||||
};
|
|
||||||
const mockVariablesService = {
|
|
||||||
getAllCached: jest.fn(() => []),
|
|
||||||
};
|
|
||||||
Container.set(LoadNodesAndCredentials, nodesAndCredentials);
|
|
||||||
Container.set(VariablesService, mockVariablesService);
|
|
||||||
mockInstance(Push);
|
|
||||||
mockInstance(SecretsHelper);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
externalHooks = mock();
|
|
||||||
activeWorkflowRunner = new ActiveWorkflowRunner(
|
|
||||||
new ActiveExecutions(),
|
|
||||||
externalHooks,
|
|
||||||
Container.get(NodeTypes),
|
|
||||||
webhookService,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow';
|
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
import { CredentialTypes } from '@/CredentialTypes';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
|
||||||
describe('CredentialTypes', () => {
|
describe('CredentialTypes', () => {
|
||||||
const mockNodesAndCredentials: INodesAndCredentials = {
|
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
|
||||||
loaded: {
|
loadedCredentials: {
|
||||||
nodes: {},
|
|
||||||
credentials: {
|
|
||||||
fakeFirstCredential: {
|
fakeFirstCredential: {
|
||||||
type: {
|
type: {
|
||||||
name: 'fakeFirstCredential',
|
name: 'fakeFirstCredential',
|
||||||
|
@ -25,12 +23,7 @@ describe('CredentialTypes', () => {
|
||||||
sourcePath: '',
|
sourcePath: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
known: { nodes: {}, credentials: {} },
|
|
||||||
credentialTypes: {} as ICredentialTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
|
|
||||||
|
|
||||||
const credentialTypes = Container.get(CredentialTypes);
|
const credentialTypes = Container.get(CredentialTypes);
|
||||||
|
|
||||||
|
@ -39,7 +32,7 @@ describe('CredentialTypes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should return correct credential type for valid name', () => {
|
test('Should return correct credential type for valid name', () => {
|
||||||
const mockedCredentialTypes = mockNodesAndCredentials.loaded.credentials;
|
const mockedCredentialTypes = mockNodesAndCredentials.loadedCredentials;
|
||||||
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
|
expect(credentialTypes.getByName('fakeFirstCredential')).toStrictEqual(
|
||||||
mockedCredentialTypes.fakeFirstCredential.type,
|
mockedCredentialTypes.fakeFirstCredential.type,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,26 +2,22 @@ import type {
|
||||||
IAuthenticateGeneric,
|
IAuthenticateGeneric,
|
||||||
ICredentialDataDecryptedObject,
|
ICredentialDataDecryptedObject,
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
ICredentialTypes,
|
|
||||||
IHttpRequestOptions,
|
IHttpRequestOptions,
|
||||||
INode,
|
INode,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodesAndCredentials,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { deepCopy } from 'n8n-workflow';
|
import { deepCopy } from 'n8n-workflow';
|
||||||
import { Workflow } from 'n8n-workflow';
|
import { Workflow } from 'n8n-workflow';
|
||||||
import { CredentialsHelper } from '@/CredentialsHelper';
|
import { CredentialsHelper } from '@/CredentialsHelper';
|
||||||
import { CredentialTypes } from '@/CredentialTypes';
|
|
||||||
import { Container } from 'typedi';
|
|
||||||
import { NodeTypes } from '@/NodeTypes';
|
import { NodeTypes } from '@/NodeTypes';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
|
||||||
describe('CredentialsHelper', () => {
|
describe('CredentialsHelper', () => {
|
||||||
const TEST_ENCRYPTION_KEY = 'test';
|
const TEST_ENCRYPTION_KEY = 'test';
|
||||||
|
|
||||||
const mockNodesAndCredentials: INodesAndCredentials = {
|
const mockNodesAndCredentials = mockInstance(LoadNodesAndCredentials, {
|
||||||
loaded: {
|
loadedNodes: {
|
||||||
nodes: {
|
|
||||||
'test.set': {
|
'test.set': {
|
||||||
sourcePath: '',
|
sourcePath: '',
|
||||||
type: {
|
type: {
|
||||||
|
@ -55,15 +51,9 @@ describe('CredentialsHelper', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
credentials: {},
|
});
|
||||||
},
|
|
||||||
known: { nodes: {}, credentials: {} },
|
|
||||||
credentialTypes: {} as ICredentialTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
Container.set(LoadNodesAndCredentials, mockNodesAndCredentials);
|
const nodeTypes = mockInstance(NodeTypes);
|
||||||
|
|
||||||
const nodeTypes = Container.get(NodeTypes);
|
|
||||||
|
|
||||||
describe('authenticate', () => {
|
describe('authenticate', () => {
|
||||||
const tests: Array<{
|
const tests: Array<{
|
||||||
|
@ -280,20 +270,14 @@ describe('CredentialsHelper', () => {
|
||||||
|
|
||||||
for (const testData of tests) {
|
for (const testData of tests) {
|
||||||
test(testData.description, async () => {
|
test(testData.description, async () => {
|
||||||
mockNodesAndCredentials.loaded.credentials = {
|
mockNodesAndCredentials.loadedCredentials = {
|
||||||
[testData.input.credentialType.name]: {
|
[testData.input.credentialType.name]: {
|
||||||
type: testData.input.credentialType,
|
type: testData.input.credentialType,
|
||||||
sourcePath: '',
|
sourcePath: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const credentialTypes = Container.get(CredentialTypes);
|
const credentialsHelper = new CredentialsHelper(TEST_ENCRYPTION_KEY);
|
||||||
|
|
||||||
const credentialsHelper = new CredentialsHelper(
|
|
||||||
TEST_ENCRYPTION_KEY,
|
|
||||||
credentialTypes,
|
|
||||||
nodeTypes,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await credentialsHelper.authenticate(
|
const result = await credentialsHelper.authenticate(
|
||||||
testData.input.credentials,
|
testData.input.credentials,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import type { ICredentialTypes, INodeTypes } from 'n8n-workflow';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { SubworkflowOperationError, Workflow } from 'n8n-workflow';
|
import type { ILogger, INodeTypes } from 'n8n-workflow';
|
||||||
|
import { LoggerProxy, SubworkflowOperationError, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
|
@ -14,35 +15,26 @@ import { UserService } from '@/services/user.service';
|
||||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||||
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
|
import * as UserManagementHelper from '@/UserManagement/UserManagementHelper';
|
||||||
import { WorkflowsService } from '@/workflows/workflows.services';
|
import { WorkflowsService } from '@/workflows/workflows.services';
|
||||||
|
import { OwnershipService } from '@/services/ownership.service';
|
||||||
|
|
||||||
|
import { mockInstance } from '../integration/shared/utils/';
|
||||||
import {
|
import {
|
||||||
randomCredentialPayload as randomCred,
|
randomCredentialPayload as randomCred,
|
||||||
randomPositiveDigit,
|
randomPositiveDigit,
|
||||||
} from '../integration/shared/random';
|
} from '../integration/shared/random';
|
||||||
import * as testDb from '../integration/shared/testDb';
|
import * as testDb from '../integration/shared/testDb';
|
||||||
import { mockNodeTypesData } from './Helpers';
|
|
||||||
import type { SaveCredentialFunction } from '../integration/shared/types';
|
import type { SaveCredentialFunction } from '../integration/shared/types';
|
||||||
import { mockInstance } from '../integration/shared/utils/';
|
import { mockNodeTypesData } from './Helpers';
|
||||||
import { OwnershipService } from '@/services/ownership.service';
|
|
||||||
|
|
||||||
import { LoggerProxy } from 'n8n-workflow';
|
LoggerProxy.init(mock<ILogger>());
|
||||||
import { getLogger } from '@/Logger';
|
|
||||||
|
|
||||||
LoggerProxy.init(getLogger());
|
|
||||||
|
|
||||||
let mockNodeTypes: INodeTypes;
|
let mockNodeTypes: INodeTypes;
|
||||||
let credentialOwnerRole: Role;
|
let credentialOwnerRole: Role;
|
||||||
let workflowOwnerRole: Role;
|
let workflowOwnerRole: Role;
|
||||||
let saveCredential: SaveCredentialFunction;
|
let saveCredential: SaveCredentialFunction;
|
||||||
|
|
||||||
const MOCK_NODE_TYPES_DATA = mockNodeTypesData(['start', 'actionNetwork']);
|
|
||||||
mockInstance(LoadNodesAndCredentials, {
|
mockInstance(LoadNodesAndCredentials, {
|
||||||
loaded: {
|
loadedNodes: mockNodeTypesData(['start', 'actionNetwork']),
|
||||||
nodes: MOCK_NODE_TYPES_DATA,
|
|
||||||
credentials: {},
|
|
||||||
},
|
|
||||||
known: { nodes: {}, credentials: {} },
|
|
||||||
credentialTypes: {} as ICredentialTypes,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { mocked } from 'jest-mock';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NODE_PACKAGE_PREFIX,
|
NODE_PACKAGE_PREFIX,
|
||||||
|
@ -9,22 +11,20 @@ import {
|
||||||
NPM_PACKAGE_STATUS_GOOD,
|
NPM_PACKAGE_STATUS_GOOD,
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
|
||||||
import { randomName } from '../../integration/shared/random';
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
|
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import { mocked } from 'jest-mock';
|
|
||||||
|
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import { CommunityPackageService } from '@/services/communityPackage.service';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
|
import { InstalledNodesRepository, InstalledPackagesRepository } from '@/databases/repositories';
|
||||||
import Container from 'typedi';
|
|
||||||
import { InstalledNodes } from '@/databases/entities/InstalledNodes';
|
import { InstalledNodes } from '@/databases/entities/InstalledNodes';
|
||||||
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COMMUNITY_NODE_VERSION,
|
COMMUNITY_NODE_VERSION,
|
||||||
COMMUNITY_PACKAGE_VERSION,
|
COMMUNITY_PACKAGE_VERSION,
|
||||||
} from '../../integration/shared/constants';
|
} from '../../integration/shared/constants';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import { randomName } from '../../integration/shared/random';
|
||||||
|
import { mockInstance, mockPackageName, mockPackagePair } from '../../integration/shared/utils';
|
||||||
|
|
||||||
jest.mock('fs/promises');
|
jest.mock('fs/promises');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
|
@ -38,10 +38,8 @@ const execMock = ((...args) => {
|
||||||
cb(null, 'Done', '');
|
cb(null, 'Done', '');
|
||||||
}) as typeof exec;
|
}) as typeof exec;
|
||||||
|
|
||||||
describe('CommunityPackageService', () => {
|
describe('CommunityPackagesService', () => {
|
||||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||||
Container.set(InstalledNodesRepository, installedNodesRepository);
|
|
||||||
|
|
||||||
installedNodesRepository.create.mockImplementation(() => {
|
installedNodesRepository.create.mockImplementation(() => {
|
||||||
const nodeName = randomName();
|
const nodeName = randomName();
|
||||||
|
|
||||||
|
@ -54,7 +52,6 @@ describe('CommunityPackageService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
|
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
|
||||||
|
|
||||||
installedPackageRepository.create.mockImplementation(() => {
|
installedPackageRepository.create.mockImplementation(() => {
|
||||||
return Object.assign(new InstalledPackages(), {
|
return Object.assign(new InstalledPackages(), {
|
||||||
packageName: mockPackageName(),
|
packageName: mockPackageName(),
|
||||||
|
@ -62,7 +59,9 @@ describe('CommunityPackageService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const communityPackageService = new CommunityPackageService(installedPackageRepository);
|
mockInstance(LoadNodesAndCredentials);
|
||||||
|
|
||||||
|
const communityPackagesService = Container.get(CommunityPackagesService);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
config.load(config.default);
|
config.load(config.default);
|
||||||
|
@ -70,18 +69,18 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
describe('parseNpmPackageName()', () => {
|
describe('parseNpmPackageName()', () => {
|
||||||
test('should fail with empty package name', () => {
|
test('should fail with empty package name', () => {
|
||||||
expect(() => communityPackageService.parseNpmPackageName('')).toThrowError();
|
expect(() => communityPackagesService.parseNpmPackageName('')).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should fail with invalid package prefix name', () => {
|
test('should fail with invalid package prefix name', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
communityPackageService.parseNpmPackageName('INVALID_PREFIX@123'),
|
communityPackagesService.parseNpmPackageName('INVALID_PREFIX@123'),
|
||||||
).toThrowError();
|
).toThrowError();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should parse valid package name', () => {
|
test('should parse valid package name', () => {
|
||||||
const name = mockPackageName();
|
const name = mockPackageName();
|
||||||
const parsed = communityPackageService.parseNpmPackageName(name);
|
const parsed = communityPackagesService.parseNpmPackageName(name);
|
||||||
|
|
||||||
expect(parsed.rawString).toBe(name);
|
expect(parsed.rawString).toBe(name);
|
||||||
expect(parsed.packageName).toBe(name);
|
expect(parsed.packageName).toBe(name);
|
||||||
|
@ -93,7 +92,7 @@ describe('CommunityPackageService', () => {
|
||||||
const name = mockPackageName();
|
const name = mockPackageName();
|
||||||
const version = '0.1.1';
|
const version = '0.1.1';
|
||||||
const fullPackageName = `${name}@${version}`;
|
const fullPackageName = `${name}@${version}`;
|
||||||
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
|
const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
|
||||||
|
|
||||||
expect(parsed.rawString).toBe(fullPackageName);
|
expect(parsed.rawString).toBe(fullPackageName);
|
||||||
expect(parsed.packageName).toBe(name);
|
expect(parsed.packageName).toBe(name);
|
||||||
|
@ -106,7 +105,7 @@ describe('CommunityPackageService', () => {
|
||||||
const name = mockPackageName();
|
const name = mockPackageName();
|
||||||
const version = '0.1.1';
|
const version = '0.1.1';
|
||||||
const fullPackageName = `${scope}/${name}@${version}`;
|
const fullPackageName = `${scope}/${name}@${version}`;
|
||||||
const parsed = communityPackageService.parseNpmPackageName(fullPackageName);
|
const parsed = communityPackagesService.parseNpmPackageName(fullPackageName);
|
||||||
|
|
||||||
expect(parsed.rawString).toBe(fullPackageName);
|
expect(parsed.rawString).toBe(fullPackageName);
|
||||||
expect(parsed.packageName).toBe(`${scope}/${name}`);
|
expect(parsed.packageName).toBe(`${scope}/${name}`);
|
||||||
|
@ -134,7 +133,7 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
mocked(exec).mockImplementation(execMock);
|
mocked(exec).mockImplementation(execMock);
|
||||||
|
|
||||||
await communityPackageService.executeNpmCommand('ls');
|
await communityPackagesService.executeNpmCommand('ls');
|
||||||
|
|
||||||
expect(fsAccess).toHaveBeenCalled();
|
expect(fsAccess).toHaveBeenCalled();
|
||||||
expect(exec).toHaveBeenCalled();
|
expect(exec).toHaveBeenCalled();
|
||||||
|
@ -144,7 +143,7 @@ describe('CommunityPackageService', () => {
|
||||||
test('should make sure folder exists', async () => {
|
test('should make sure folder exists', async () => {
|
||||||
mocked(exec).mockImplementation(execMock);
|
mocked(exec).mockImplementation(execMock);
|
||||||
|
|
||||||
await communityPackageService.executeNpmCommand('ls');
|
await communityPackagesService.executeNpmCommand('ls');
|
||||||
expect(fsAccess).toHaveBeenCalled();
|
expect(fsAccess).toHaveBeenCalled();
|
||||||
expect(exec).toHaveBeenCalled();
|
expect(exec).toHaveBeenCalled();
|
||||||
expect(fsMkdir).toBeCalledTimes(0);
|
expect(fsMkdir).toBeCalledTimes(0);
|
||||||
|
@ -156,7 +155,7 @@ describe('CommunityPackageService', () => {
|
||||||
throw new Error('Folder does not exist.');
|
throw new Error('Folder does not exist.');
|
||||||
});
|
});
|
||||||
|
|
||||||
await communityPackageService.executeNpmCommand('ls');
|
await communityPackagesService.executeNpmCommand('ls');
|
||||||
|
|
||||||
expect(fsAccess).toHaveBeenCalled();
|
expect(fsAccess).toHaveBeenCalled();
|
||||||
expect(exec).toHaveBeenCalled();
|
expect(exec).toHaveBeenCalled();
|
||||||
|
@ -172,7 +171,7 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
mocked(exec).mockImplementation(erroringExecMock);
|
mocked(exec).mockImplementation(erroringExecMock);
|
||||||
|
|
||||||
const call = async () => communityPackageService.executeNpmCommand('ls');
|
const call = async () => communityPackagesService.executeNpmCommand('ls');
|
||||||
|
|
||||||
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
|
await expect(call).rejects.toThrowError(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
|
||||||
|
|
||||||
|
@ -186,7 +185,7 @@ describe('CommunityPackageService', () => {
|
||||||
test('should return same list if availableUpdates is undefined', () => {
|
test('should return same list if availableUpdates is undefined', () => {
|
||||||
const fakePkgs = mockPackagePair();
|
const fakePkgs = mockPackagePair();
|
||||||
|
|
||||||
const crossedPkgs = communityPackageService.matchPackagesWithUpdates(fakePkgs);
|
const crossedPkgs = communityPackagesService.matchPackagesWithUpdates(fakePkgs);
|
||||||
|
|
||||||
expect(crossedPkgs).toEqual(fakePkgs);
|
expect(crossedPkgs).toEqual(fakePkgs);
|
||||||
});
|
});
|
||||||
|
@ -210,7 +209,7 @@ describe('CommunityPackageService', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
||||||
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
||||||
|
|
||||||
expect(crossedPkgA.updateAvailable).toBe('0.2.0');
|
expect(crossedPkgA.updateAvailable).toBe('0.2.0');
|
||||||
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
||||||
|
@ -229,7 +228,7 @@ describe('CommunityPackageService', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
const [crossedPkgA, crossedPkgB]: PublicInstalledPackage[] =
|
||||||
communityPackageService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
communityPackagesService.matchPackagesWithUpdates([pkgA, pkgB], updates);
|
||||||
|
|
||||||
expect(crossedPkgA.updateAvailable).toBeUndefined();
|
expect(crossedPkgA.updateAvailable).toBeUndefined();
|
||||||
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
expect(crossedPkgB.updateAvailable).toBe('0.3.0');
|
||||||
|
@ -239,12 +238,12 @@ describe('CommunityPackageService', () => {
|
||||||
describe('matchMissingPackages()', () => {
|
describe('matchMissingPackages()', () => {
|
||||||
test('should not match failed packages that do not exist', () => {
|
test('should not match failed packages that do not exist', () => {
|
||||||
const fakePkgs = mockPackagePair();
|
const fakePkgs = mockPackagePair();
|
||||||
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`;
|
setMissingPackages([
|
||||||
|
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
|
||||||
|
`${NODE_PACKAGE_PREFIX}another-very-long-name-that-never-is-seen`,
|
||||||
|
]);
|
||||||
|
|
||||||
const matchedPackages = communityPackageService.matchMissingPackages(
|
const matchedPackages = communityPackagesService.matchMissingPackages(fakePkgs);
|
||||||
fakePkgs,
|
|
||||||
notFoundPkgNames,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(matchedPackages).toEqual(fakePkgs);
|
expect(matchedPackages).toEqual(fakePkgs);
|
||||||
|
|
||||||
|
@ -256,12 +255,15 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
test('should match failed packages that should be present', () => {
|
test('should match failed packages that should be present', () => {
|
||||||
const [pkgA, pkgB] = mockPackagePair();
|
const [pkgA, pkgB] = mockPackagePair();
|
||||||
const notFoundPkgNames = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@${pkgA.installedVersion}`;
|
setMissingPackages([
|
||||||
|
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
|
||||||
|
`${pkgA.packageName}@${pkgA.installedVersion}`,
|
||||||
|
]);
|
||||||
|
|
||||||
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
|
const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([
|
||||||
[pkgA, pkgB],
|
pkgA,
|
||||||
notFoundPkgNames,
|
pkgB,
|
||||||
);
|
]);
|
||||||
|
|
||||||
expect(matchedPkgA.failedLoading).toBe(true);
|
expect(matchedPkgA.failedLoading).toBe(true);
|
||||||
expect(matchedPkgB.failedLoading).toBeUndefined();
|
expect(matchedPkgB.failedLoading).toBeUndefined();
|
||||||
|
@ -269,11 +271,14 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
test('should match failed packages even if version is wrong', () => {
|
test('should match failed packages even if version is wrong', () => {
|
||||||
const [pkgA, pkgB] = mockPackagePair();
|
const [pkgA, pkgB] = mockPackagePair();
|
||||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${pkgA.packageName}@123.456.789`;
|
setMissingPackages([
|
||||||
const [matchedPkgA, matchedPkgB] = communityPackageService.matchMissingPackages(
|
`${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0`,
|
||||||
[pkgA, pkgB],
|
`${pkgA.packageName}@123.456.789`,
|
||||||
notFoundPackageList,
|
]);
|
||||||
);
|
const [matchedPkgA, matchedPkgB] = communityPackagesService.matchMissingPackages([
|
||||||
|
pkgA,
|
||||||
|
pkgB,
|
||||||
|
]);
|
||||||
|
|
||||||
expect(matchedPkgA.failedLoading).toBe(true);
|
expect(matchedPkgA.failedLoading).toBe(true);
|
||||||
expect(matchedPkgB.failedLoading).toBeUndefined();
|
expect(matchedPkgB.failedLoading).toBeUndefined();
|
||||||
|
@ -282,7 +287,7 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
describe('checkNpmPackageStatus()', () => {
|
describe('checkNpmPackageStatus()', () => {
|
||||||
test('should call axios.post', async () => {
|
test('should call axios.post', async () => {
|
||||||
await communityPackageService.checkNpmPackageStatus(mockPackageName());
|
await communityPackagesService.checkNpmPackageStatus(mockPackageName());
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalled();
|
expect(axios.post).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -292,7 +297,7 @@ describe('CommunityPackageService', () => {
|
||||||
throw new Error('Something went wrong');
|
throw new Error('Something went wrong');
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await communityPackageService.checkNpmPackageStatus(mockPackageName());
|
const result = await communityPackagesService.checkNpmPackageStatus(mockPackageName());
|
||||||
|
|
||||||
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
||||||
});
|
});
|
||||||
|
@ -300,7 +305,7 @@ describe('CommunityPackageService', () => {
|
||||||
test('should warn if package is banned', async () => {
|
test('should warn if package is banned', async () => {
|
||||||
mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } });
|
mocked(axios.post).mockResolvedValue({ data: { status: 'Banned', reason: 'Not good' } });
|
||||||
|
|
||||||
const result = (await communityPackageService.checkNpmPackageStatus(
|
const result = (await communityPackagesService.checkNpmPackageStatus(
|
||||||
mockPackageName(),
|
mockPackageName(),
|
||||||
)) as CommunityPackages.PackageStatusCheck;
|
)) as CommunityPackages.PackageStatusCheck;
|
||||||
|
|
||||||
|
@ -311,47 +316,50 @@ describe('CommunityPackageService', () => {
|
||||||
|
|
||||||
describe('hasPackageLoadedSuccessfully()', () => {
|
describe('hasPackageLoadedSuccessfully()', () => {
|
||||||
test('should return true when failed package list does not exist', () => {
|
test('should return true when failed package list does not exist', () => {
|
||||||
config.set<string>('nodes.packagesMissing', undefined);
|
setMissingPackages([]);
|
||||||
|
expect(communityPackagesService.hasPackageLoaded('package')).toBe(true);
|
||||||
expect(communityPackageService.hasPackageLoaded('package')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return true when package is not in the list of missing packages', () => {
|
test('should return true when package is not in the list of missing packages', () => {
|
||||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
|
setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']);
|
||||||
|
expect(communityPackagesService.hasPackageLoaded('packageC')).toBe(true);
|
||||||
expect(communityPackageService.hasPackageLoaded('packageC')).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return false when package is in the list of missing packages', () => {
|
test('should return false when package is in the list of missing packages', () => {
|
||||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.1.0');
|
setMissingPackages(['packageA@0.1.0', 'packageB@0.1.0']);
|
||||||
|
expect(communityPackagesService.hasPackageLoaded('packageA')).toBe(false);
|
||||||
expect(communityPackageService.hasPackageLoaded('packageA')).toBe(false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removePackageFromMissingList()', () => {
|
describe('removePackageFromMissingList()', () => {
|
||||||
test('should do nothing if key does not exist', () => {
|
test('should do nothing if key does not exist', () => {
|
||||||
config.set<string>('nodes.packagesMissing', undefined);
|
setMissingPackages([]);
|
||||||
|
communityPackagesService.removePackageFromMissingList('packageA');
|
||||||
|
|
||||||
communityPackageService.removePackageFromMissingList('packageA');
|
expect(communityPackagesService.missingPackages).toBeEmptyArray();
|
||||||
|
|
||||||
expect(config.get('nodes.packagesMissing')).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove only correct package from list', () => {
|
test('should remove only correct package from list', () => {
|
||||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageC@0.2.0');
|
setMissingPackages(['packageA@0.1.0', 'packageB@0.2.0', 'packageC@0.2.0']);
|
||||||
|
|
||||||
communityPackageService.removePackageFromMissingList('packageB');
|
communityPackagesService.removePackageFromMissingList('packageB');
|
||||||
|
|
||||||
expect(config.get('nodes.packagesMissing')).toBe('packageA@0.1.0 packageC@0.2.0');
|
expect(communityPackagesService.missingPackages).toEqual([
|
||||||
|
'packageA@0.1.0',
|
||||||
|
'packageC@0.2.0',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not remove if package is not in the list', () => {
|
test('should not remove if package is not in the list', () => {
|
||||||
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageB@0.2.0';
|
const failedToLoadList = ['packageA@0.1.0', 'packageB@0.2.0', 'packageB@0.2.0'];
|
||||||
config.set('nodes.packagesMissing', failedToLoadList);
|
setMissingPackages(failedToLoadList);
|
||||||
communityPackageService.removePackageFromMissingList('packageC');
|
communityPackagesService.removePackageFromMissingList('packageC');
|
||||||
|
|
||||||
expect(config.get('nodes.packagesMissing')).toBe(failedToLoadList);
|
expect(communityPackagesService.missingPackages).toEqual(failedToLoadList);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setMissingPackages = (missingPackages: string[]) => {
|
||||||
|
Object.assign(communityPackagesService, { missingPackages });
|
||||||
|
};
|
||||||
});
|
});
|
|
@ -5,7 +5,7 @@ import { get, post, makeRestApiRequest } from '@/utils';
|
||||||
export async function getInstalledCommunityNodes(
|
export async function getInstalledCommunityNodes(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
): Promise<PublicInstalledPackage[]> {
|
): Promise<PublicInstalledPackage[]> {
|
||||||
const response = await get(context.baseUrl, '/nodes');
|
const response = await get(context.baseUrl, '/community-packages');
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,16 +13,16 @@ export async function installNewPackage(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<PublicInstalledPackage> {
|
): Promise<PublicInstalledPackage> {
|
||||||
return post(context.baseUrl, '/nodes', { name });
|
return post(context.baseUrl, '/community-packages', { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> {
|
export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> {
|
||||||
return makeRestApiRequest(context, 'DELETE', '/nodes', { name });
|
return makeRestApiRequest(context, 'DELETE', '/community-packages', { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePackage(
|
export async function updatePackage(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<PublicInstalledPackage> {
|
): Promise<PublicInstalledPackage> {
|
||||||
return makeRestApiRequest(context, 'PATCH', '/nodes', { name });
|
return makeRestApiRequest(context, 'PATCH', '/community-packages', { name });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1712,17 +1712,6 @@ type LoadedData<T> = Record<string, LoadedClass<T>>;
|
||||||
export type ICredentialTypeData = LoadedData<ICredentialType>;
|
export type ICredentialTypeData = LoadedData<ICredentialType>;
|
||||||
export type INodeTypeData = LoadedData<INodeType | IVersionedNodeType>;
|
export type INodeTypeData = LoadedData<INodeType | IVersionedNodeType>;
|
||||||
|
|
||||||
export type LoadedNodesAndCredentials = {
|
|
||||||
nodes: INodeTypeData;
|
|
||||||
credentials: ICredentialTypeData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface INodesAndCredentials {
|
|
||||||
known: KnownNodesAndCredentials;
|
|
||||||
loaded: LoadedNodesAndCredentials;
|
|
||||||
credentialTypes: ICredentialTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRun {
|
export interface IRun {
|
||||||
data: IRunExecutionData;
|
data: IRunExecutionData;
|
||||||
finished?: boolean;
|
finished?: boolean;
|
||||||
|
|
Loading…
Reference in a new issue