* ✨ Make it possible to dynamically load node packages * ⚡ Fix comment * ✨ Make possible to dynamically install nodes from npm * Created migration for sqlite regarding community nodes * Saving to db whenever a package with nodes is installed * Created endpoint to fetch installed packages * WIP - uninstall package with nodes * Fix lint issues * Updating nodes via API * Lint and improvement fixes * Created community node helpers and removed packages taht do not contain nodes * Check for package updates when fetching installed packages * Blocked access to non-owner and preventing incorrect install of packages * Added auto healing process * Unit tests for helpers * Finishing tests for helpers * Improved unit tests, refactored more helpers and created integration tests for GET * Implemented detection of missing packages on init and added warning to frontend settings * Add check for banned packages and fix broken tests * Create migrations for other db systems * Updated with latest changes from master * Fixed conflict errors * Improved unit tests, refactored more helpers and created integration tests for GET * Implemented detection of missing packages on init and added warning to frontend settings * 🔥 Removing access check for the Settings sidebar item * ✨ Added inital community nodes settings screen * ⚡Added executionMode flag to settings * ✨ Implemented N8N-callout component * 💄Updating Callout component template propery names * 💄 Updating Callout component styling. * 💄Updating Callout component sizing and colors. * ✔️ Updating Callout component test snapshots after styling changes * ✨ Updating the `ActionBox` component so it supports callouts and conditional button rendering * 💄 Removing duplicate callout theme validation in the `ActionBox` component. Adding a selection control for it in the storybook. * ✨ Added warning message if instance is in the queue mode. Updated colors based on the new design. * ⚡ Added a custom permission support to router * 🔨 Implemented UM detection as a custom permission. * 👌Updating route permission logic. * ✨ Implemented installed community packages list in the settings view * 👌 Updating settings routes rules and community nodes setting view. * Allow installation of packages that failed to load * 👌 Updating `ActionBox`, `CommuntyPackageCard` components and settings loading logic. * 👌 Fixing community nodes loading state and sidebar icon spacing. * ✨ Implemented loading skeletons for community package cards * 👌 Handling errrors while loading installed package list. Updating spacing. * 👌 Updating community nodes error messages. * Added disable flag * 🐛 Fixing a community nodes update detection bug when there are missing packages. (#3497) * ✨ Added front-end support for community nodes feature flag * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * Standardize error codes (#3501) * Standardize error: 400 for request issues such as invalid package name and 500 for installation problems * Fix http status code for when package is not found * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * ✨ Updated error handling based on the response codes * ✨ Implemented community package installation modal dialog * ✨ Implemented community package uninstall flow. * ✨ Finished update confirm modal UI * 💄 Replaced community nodes tooltip image with the one exported from figma. * ✨ Implemented community package update process * ✨ Updating community nodes list after successful package update * 🔒 Updating public API setting route to use new access rules. Updating express app definition in community nodes tests * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * ✨ Updated error handling based on the response codes * Change output for installation request * Improve payload for update requests * 👌 Updating community nodes install modal UI * 👌 Updating community nodes confirm modal logic * 👌 Refactoring community nodes confirm modal dialog * 👌 Separating community nodes components loading states * 💄 Updating community nodes install modal spacing. * Fix behavior for installing already installed packages * 💡 Commenting community nodes install process * 🔥 Removing leftover commits of deleted Vue mutations * ✨ Updated node list to identify community nodes and handle node name clash * ✨ Implemented missing community node dialog. * 💄 Updating n8n-tabs component to support tooltips * ✨ Updating node details with community node details. * 🔨 Using back-end response when updating community packages * 👌 Updating tabs component and refactoring community nodes store mutations * 👌 Adding community node flag to node type descriptions and using it to identify community nodes * 👌 Hiding unnecessary elements from missing node details panel. * 👌 Updating missing node type descriptions for custom and community nodes * 👌 Updating community node package name detection logic * 👌 Removing communityNode flag from node description * ✨ Adding `force` flag to credentials fetching (#3527) * ✨ Adding `force` flag to credentials fetching which can be used to skip check when loading credentials * ✨ Forcing credentials loading when opening nodeView * 👌 Minor updates to community nodes details panel * tests for post endpoint * duplicate comments * Add Patch and Delete enpoints tests * 🔒 Using `pageCategory`prop to assemble the list of settings routes instead of hard-coded array (#3562) * 📈 Added front-end telemetry events for community nodes * 📈 Updating community nodes telemetry events * 💄 Updating community nodes settings UI elements based on product/design review * 💄 Updating node view & node details view for community nodes based on product/design feedback * 💄 Fixing community node text capitalisation * ✨ Adding community node install error message under the package name input field * Fixed and improved tests * Fix lint issue * feat: Migrated to npm release of riot-tmpl fork. * 📈 Updating community nodes telemetry events based on the product review * 💄 Updating community nodes UI based on the design feedback * 🔀 Merging recent node draggable panels changes * Implement self healing process * Improve error messages for package name requirement and disk space * 💄 Removing front-end error message override since appropriate response is available from the back-end * Fix lint issues * Fix installed node name * 💄 Removed additional node name parsing * 📈 Updating community nodes telemetry events * Fix postgres migration for cascading nodes when package is removed * Remove postman mock for banned packages * 📈 Adding missing telemetry event for community node documentation click * 🐛 Fixing community nodes UI bugs reported during the bug bash * Fix issue with uninstalling packages not reflecting UI * 🐛 Fixing a missing node type bug when trying to run a workflow. * Improve error detection for installing packages * 💄 Updating community nodes components styling and wording based on the product feedback * Implement telemetry be events * Add author name and email to packages * Fix telemetry be events for community packages * 📈 Updating front-end telemetry events with community nodes author data * 💄 Updating credentials documentation link logic to handle community nodes credentials * 🐛 Fixing draggable panels logic * Fix duplicate wrong import * 💄 Hiding community nodes credentials documentation links when they don't contain an absolute URL * Fix issue with detection of missing packages * 💄 Adding the `Docs` tab to community nodes * 💄 Adding a failed loading indicator to community nodes list * Prevent n8n from crashing on startup * Refactor and improve code quality * ⚡ Remove not needed depenedency Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Milorad Filipović <milorad@n8n.io> Co-authored-by: Milorad FIlipović <miloradfilipovic19@gmail.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com> Co-authored-by: Alex Grozav <alex@grozav.com>
616 lines
19 KiB
TypeScript
616 lines
19 KiB
TypeScript
/* eslint-disable import/no-cycle */
|
|
/* eslint-disable no-underscore-dangle */
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
/* eslint-disable no-prototype-builtins */
|
|
/* eslint-disable no-param-reassign */
|
|
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
/* eslint-disable no-await-in-loop */
|
|
/* eslint-disable no-continue */
|
|
/* eslint-disable no-restricted-syntax */
|
|
import { CUSTOM_EXTENSION_ENV, UserSettings } from 'n8n-core';
|
|
import {
|
|
CodexData,
|
|
ICredentialType,
|
|
ICredentialTypeData,
|
|
ILogger,
|
|
INodeType,
|
|
INodeTypeData,
|
|
INodeTypeNameVersion,
|
|
INodeVersionedType,
|
|
LoggerProxy,
|
|
} from 'n8n-workflow';
|
|
|
|
import {
|
|
access as fsAccess,
|
|
readdir as fsReaddir,
|
|
readFile as fsReadFile,
|
|
stat as fsStat,
|
|
} from 'fs/promises';
|
|
import glob from 'fast-glob';
|
|
import path from 'path';
|
|
import { IN8nNodePackageJson } from './Interfaces';
|
|
import { getLogger } from './Logger';
|
|
import config from '../config';
|
|
import { NodeTypes } from '.';
|
|
import { InstalledPackages } from './databases/entities/InstalledPackages';
|
|
import { InstalledNodes } from './databases/entities/InstalledNodes';
|
|
import { executeCommand } from './CommunityNodes/helpers';
|
|
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
|
import {
|
|
persistInstalledPackageData,
|
|
removePackageFromDatabase,
|
|
} from './CommunityNodes/packageModel';
|
|
|
|
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
|
|
|
class LoadNodesAndCredentialsClass {
|
|
nodeTypes: INodeTypeData = {};
|
|
|
|
credentialTypes: ICredentialTypeData = {};
|
|
|
|
excludeNodes: string | undefined = undefined;
|
|
|
|
includeNodes: string | undefined = undefined;
|
|
|
|
nodeModulesPath = '';
|
|
|
|
logger: ILogger;
|
|
|
|
async init() {
|
|
this.logger = getLogger();
|
|
LoggerProxy.init(this.logger);
|
|
|
|
// Make sure the imported modules can resolve dependencies fine.
|
|
process.env.NODE_PATH = module.paths.join(':');
|
|
// @ts-ignore
|
|
module.constructor._initPaths();
|
|
|
|
this.nodeModulesPath = await this.getNodeModulesFolderLocation();
|
|
|
|
this.excludeNodes = config.getEnv('nodes.exclude');
|
|
this.includeNodes = config.getEnv('nodes.include');
|
|
|
|
// Get all the installed packages which contain n8n nodes
|
|
const nodePackages = await this.getN8nNodePackages(this.nodeModulesPath);
|
|
|
|
for (const packagePath of nodePackages) {
|
|
await this.loadDataFromPackage(packagePath);
|
|
}
|
|
|
|
await this.loadNodesFromDownloadedPackages();
|
|
|
|
await this.loadNodesFromCustomFolders();
|
|
}
|
|
|
|
async getNodeModulesFolderLocation(): Promise<string> {
|
|
// Get the path to the node-modules folder to be later able
|
|
// to load the credentials and nodes
|
|
const checkPaths = [
|
|
// In case "n8n" package is in same node_modules folder.
|
|
path.join(__dirname, '..', '..', '..', 'n8n-workflow'),
|
|
// In case "n8n" package is the root and the packages are
|
|
// in the "node_modules" folder underneath it.
|
|
path.join(__dirname, '..', '..', 'node_modules', 'n8n-workflow'),
|
|
];
|
|
for (const checkPath of checkPaths) {
|
|
try {
|
|
await fsAccess(checkPath);
|
|
// Folder exists, so use it.
|
|
return path.dirname(checkPath);
|
|
} catch (error) {
|
|
// Folder does not exist so get next one
|
|
// eslint-disable-next-line no-continue
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error('Could not find "node_modules" folder!');
|
|
}
|
|
|
|
async loadNodesFromDownloadedPackages(): Promise<void> {
|
|
const nodePackages = [];
|
|
try {
|
|
// Read downloaded nodes and credentials
|
|
const downloadedNodesFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
|
const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules');
|
|
await fsAccess(downloadedNodesFolderModules);
|
|
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules);
|
|
nodePackages.push(...downloadedPackages);
|
|
// eslint-disable-next-line no-empty
|
|
} catch (error) {}
|
|
|
|
for (const packagePath of nodePackages) {
|
|
try {
|
|
await this.loadDataFromPackage(packagePath);
|
|
// eslint-disable-next-line no-empty
|
|
} catch (error) {}
|
|
}
|
|
}
|
|
|
|
async loadNodesFromCustomFolders(): Promise<void> {
|
|
// Read nodes and credentials from custom directories
|
|
const customDirectories = [];
|
|
|
|
// Add "custom" folder in user-n8n folder
|
|
customDirectories.push(UserSettings.getUserN8nFolderCustomExtensionPath());
|
|
|
|
// Add folders from special environment variable
|
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
|
|
// eslint-disable-next-line prefer-spread
|
|
customDirectories.push.apply(customDirectories, customExtensionFolders);
|
|
}
|
|
|
|
for (const directory of customDirectories) {
|
|
await this.loadDataFromDirectory('CUSTOM', directory);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns all the names of the packages which could
|
|
* contain n8n nodes
|
|
*
|
|
* @returns {Promise<string[]>}
|
|
* @memberof LoadNodesAndCredentialsClass
|
|
*/
|
|
async getN8nNodePackages(baseModulesPath: string): Promise<string[]> {
|
|
const getN8nNodePackagesRecursive = async (relativePath: string): Promise<string[]> => {
|
|
const results: string[] = [];
|
|
const nodeModulesPath = `${baseModulesPath}/${relativePath}`;
|
|
for (const file of await fsReaddir(nodeModulesPath)) {
|
|
const isN8nNodesPackage = file.indexOf('n8n-nodes-') === 0;
|
|
const isNpmScopedPackage = file.indexOf('@') === 0;
|
|
if (!isN8nNodesPackage && !isNpmScopedPackage) {
|
|
continue;
|
|
}
|
|
if (!(await fsStat(nodeModulesPath)).isDirectory()) {
|
|
continue;
|
|
}
|
|
if (isN8nNodesPackage) {
|
|
results.push(`${baseModulesPath}/${relativePath}${file}`);
|
|
}
|
|
if (isNpmScopedPackage) {
|
|
results.push(...(await getN8nNodePackagesRecursive(`${relativePath}${file}/`)));
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
return getN8nNodePackagesRecursive('');
|
|
}
|
|
|
|
/**
|
|
* Loads credentials from a file
|
|
*
|
|
* @param {string} credentialName The name of the credentials
|
|
* @param {string} filePath The file to read credentials from
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
|
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
const tempModule = require(filePath);
|
|
|
|
let tempCredential: ICredentialType;
|
|
try {
|
|
// Add serializer method "toJSON" to the class so that authenticate method (if defined)
|
|
// gets mapped to the authenticate attribute before it is sent to the client.
|
|
// The authenticate property is used by the client to decide whether or not to
|
|
// include the credential type in the predifined credentials (HTTP node)
|
|
// eslint-disable-next-line func-names
|
|
tempModule[credentialName].prototype.toJSON = function () {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return {
|
|
...this,
|
|
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
|
|
};
|
|
};
|
|
|
|
tempCredential = new tempModule[credentialName]() as ICredentialType;
|
|
|
|
if (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
|
|
// If a file icon gets used add the full path
|
|
tempCredential.icon = `file:${path.join(
|
|
path.dirname(filePath),
|
|
tempCredential.icon.substr(5),
|
|
)}`;
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof TypeError) {
|
|
throw new Error(
|
|
`Class with name "${credentialName}" could not be found. Please check if the class is named correctly!`,
|
|
);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
this.credentialTypes[tempCredential.name] = {
|
|
type: tempCredential,
|
|
sourcePath: filePath,
|
|
};
|
|
}
|
|
|
|
async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
|
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
|
const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
|
|
|
|
await executeCommand(command);
|
|
|
|
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
|
|
|
const loadedNodes = await this.loadDataFromPackage(finalNodeUnpackedPath);
|
|
|
|
if (loadedNodes.length > 0) {
|
|
const packageFile = await this.readPackageJson(finalNodeUnpackedPath);
|
|
// Save info to DB
|
|
try {
|
|
const installedPackage = await persistInstalledPackageData(
|
|
packageFile.name,
|
|
packageFile.version,
|
|
loadedNodes,
|
|
this.nodeTypes,
|
|
packageFile.author?.name,
|
|
packageFile.author?.email,
|
|
);
|
|
this.attachNodesToNodeTypes(installedPackage.installedNodes);
|
|
return installedPackage;
|
|
} catch (error) {
|
|
LoggerProxy.error('Failed to save installed packages and nodes', { error, packageName });
|
|
throw error;
|
|
}
|
|
} else {
|
|
// Remove this package since it contains no loadable nodes
|
|
const removeCommand = `npm remove ${packageName}`;
|
|
try {
|
|
await executeCommand(removeCommand);
|
|
} catch (error) {
|
|
// Do nothing
|
|
}
|
|
|
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
|
}
|
|
}
|
|
|
|
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
|
const command = `npm remove ${packageName}`;
|
|
|
|
await executeCommand(command);
|
|
|
|
void (await removePackageFromDatabase(installedPackage));
|
|
|
|
this.unloadNodes(installedPackage.installedNodes);
|
|
}
|
|
|
|
async updateNpmModule(
|
|
packageName: string,
|
|
installedPackage: InstalledPackages,
|
|
): Promise<InstalledPackages> {
|
|
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
|
|
|
const command = `npm update ${packageName}`;
|
|
|
|
try {
|
|
await executeCommand(command);
|
|
} catch (error) {
|
|
if (error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
|
|
throw new Error(`The npm package "${packageName}" could not be found.`);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
this.unloadNodes(installedPackage.installedNodes);
|
|
|
|
const finalNodeUnpackedPath = path.join(downloadFolder, 'node_modules', packageName);
|
|
|
|
const loadedNodes = await this.loadDataFromPackage(finalNodeUnpackedPath);
|
|
|
|
if (loadedNodes.length > 0) {
|
|
const packageFile = await this.readPackageJson(finalNodeUnpackedPath);
|
|
|
|
// Save info to DB
|
|
try {
|
|
await removePackageFromDatabase(installedPackage);
|
|
|
|
const newlyInstalledPackage = await persistInstalledPackageData(
|
|
packageFile.name,
|
|
packageFile.version,
|
|
loadedNodes,
|
|
this.nodeTypes,
|
|
packageFile.author?.name,
|
|
packageFile.author?.email,
|
|
);
|
|
|
|
this.attachNodesToNodeTypes(newlyInstalledPackage.installedNodes);
|
|
|
|
return newlyInstalledPackage;
|
|
} catch (error) {
|
|
LoggerProxy.error('Failed to save installed packages and nodes', { error, packageName });
|
|
throw error;
|
|
}
|
|
} else {
|
|
// Remove this package since it contains no loadable nodes
|
|
const removeCommand = `npm remove ${packageName}`;
|
|
try {
|
|
await executeCommand(removeCommand);
|
|
} catch (error) {
|
|
// Do nothing
|
|
}
|
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a node from a file
|
|
*
|
|
* @param {string} packageName The package name to set for the found nodes
|
|
* @param {string} nodeName Tha name of the node
|
|
* @param {string} filePath The file to read node from
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async loadNodeFromFile(
|
|
packageName: string,
|
|
nodeName: string,
|
|
filePath: string,
|
|
): Promise<INodeTypeNameVersion | undefined> {
|
|
let tempNode: INodeType | INodeVersionedType;
|
|
let fullNodeName: string;
|
|
let nodeVersion = 1;
|
|
|
|
try {
|
|
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
const tempModule = require(filePath);
|
|
tempNode = new tempModule[nodeName]();
|
|
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
|
} catch (error) {
|
|
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
|
|
console.error(`Error loading node "${nodeName}" from: "${filePath}" - ${error.message}`);
|
|
throw error;
|
|
}
|
|
|
|
// eslint-disable-next-line prefer-const
|
|
fullNodeName = `${packageName}.${tempNode.description.name}`;
|
|
tempNode.description.name = fullNodeName;
|
|
|
|
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
|
|
// If a file icon gets used add the full path
|
|
tempNode.description.icon = `file:${path.join(
|
|
path.dirname(filePath),
|
|
tempNode.description.icon.substr(5),
|
|
)}`;
|
|
}
|
|
|
|
if (tempNode.hasOwnProperty('executeSingle')) {
|
|
this.logger.warn(
|
|
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
|
|
{ filePath },
|
|
);
|
|
}
|
|
|
|
if (tempNode.hasOwnProperty('nodeVersions')) {
|
|
const versionedNodeType = (tempNode as INodeVersionedType).getNodeType();
|
|
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
|
|
nodeVersion = (tempNode as INodeVersionedType).currentVersion;
|
|
|
|
if (
|
|
versionedNodeType.description.icon !== undefined &&
|
|
versionedNodeType.description.icon.startsWith('file:')
|
|
) {
|
|
// If a file icon gets used add the full path
|
|
versionedNodeType.description.icon = `file:${path.join(
|
|
path.dirname(filePath),
|
|
versionedNodeType.description.icon.substr(5),
|
|
)}`;
|
|
}
|
|
|
|
if (versionedNodeType.hasOwnProperty('executeSingle')) {
|
|
this.logger.warn(
|
|
`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`,
|
|
{ filePath },
|
|
);
|
|
}
|
|
} else {
|
|
// Short renaming to avoid type issues
|
|
const tmpNode = tempNode as INodeType;
|
|
nodeVersion = Array.isArray(tmpNode.description.version)
|
|
? tmpNode.description.version.slice(-1)[0]
|
|
: tmpNode.description.version;
|
|
}
|
|
|
|
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
|
|
return;
|
|
}
|
|
|
|
// Check if the node should be skiped
|
|
if (this.excludeNodes !== undefined && this.excludeNodes.includes(fullNodeName)) {
|
|
return;
|
|
}
|
|
|
|
this.nodeTypes[fullNodeName] = {
|
|
type: tempNode,
|
|
sourcePath: filePath,
|
|
};
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
return {
|
|
name: fullNodeName,
|
|
version: nodeVersion,
|
|
} as INodeTypeNameVersion;
|
|
}
|
|
|
|
/**
|
|
* Retrieves `categories`, `subcategories` and alias (if defined)
|
|
* from the codex data for the node at the given file path.
|
|
*
|
|
* @param {string} filePath The file path to a `*.node.js` file
|
|
* @returns {CodexData}
|
|
*/
|
|
getCodex(filePath: string): CodexData {
|
|
// eslint-disable-next-line global-require, import/no-dynamic-require, @typescript-eslint/no-var-requires
|
|
const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
return {
|
|
...(categories && { categories }),
|
|
...(subcategories && { subcategories }),
|
|
...(alias && { alias }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adds a node codex `categories` and `subcategories` (if defined)
|
|
* to a node description `codex` property.
|
|
*
|
|
* @param {object} obj
|
|
* @param obj.node Node to add categories to
|
|
* @param obj.filePath Path to the built node
|
|
* @param obj.isCustom Whether the node is custom
|
|
* @returns {void}
|
|
*/
|
|
addCodex({
|
|
node,
|
|
filePath,
|
|
isCustom,
|
|
}: {
|
|
node: INodeType | INodeVersionedType;
|
|
filePath: string;
|
|
isCustom: boolean;
|
|
}) {
|
|
try {
|
|
const codex = this.getCodex(filePath);
|
|
|
|
if (isCustom) {
|
|
codex.categories = codex.categories
|
|
? codex.categories.concat(CUSTOM_NODES_CATEGORY)
|
|
: [CUSTOM_NODES_CATEGORY];
|
|
}
|
|
|
|
node.description.codex = codex;
|
|
} catch (_) {
|
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
|
|
|
|
if (isCustom) {
|
|
node.description.codex = {
|
|
categories: [CUSTOM_NODES_CATEGORY],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads nodes and credentials from the given directory
|
|
*
|
|
* @param {string} setPackageName The package name to set for the found nodes
|
|
* @param {string} directory The directory to look in
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
|
const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
|
|
|
|
let fileName: string;
|
|
let type: string;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const loadPromises: any[] = [];
|
|
for (const filePath of files) {
|
|
[fileName, type] = path.parse(filePath).name.split('.');
|
|
|
|
if (type === 'node') {
|
|
loadPromises.push(this.loadNodeFromFile(setPackageName, fileName, filePath));
|
|
} else if (type === 'credentials') {
|
|
loadPromises.push(this.loadCredentialsFromFile(fileName, filePath));
|
|
}
|
|
}
|
|
|
|
await Promise.all(loadPromises);
|
|
}
|
|
|
|
async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> {
|
|
// Get the absolute path of the package
|
|
const packageFileString = await fsReadFile(path.join(packagePath, 'package.json'), 'utf8');
|
|
return JSON.parse(packageFileString) as IN8nNodePackageJson;
|
|
}
|
|
|
|
/**
|
|
* Loads nodes and credentials from the package with the given name
|
|
*
|
|
* @param {string} packagePath The path to read data from
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> {
|
|
// Get the absolute path of the package
|
|
const packageFile = await this.readPackageJson(packagePath);
|
|
// if (!packageFile.hasOwnProperty('n8n')) {
|
|
if (!packageFile.n8n) {
|
|
return [];
|
|
}
|
|
|
|
const packageName = packageFile.name;
|
|
|
|
let tempPath: string;
|
|
let filePath: string;
|
|
|
|
const returnData: INodeTypeNameVersion[] = [];
|
|
|
|
// Read all node types
|
|
let fileName: string;
|
|
let type: string;
|
|
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
|
|
for (filePath of packageFile.n8n.nodes) {
|
|
tempPath = path.join(packagePath, filePath);
|
|
[fileName, type] = path.parse(filePath).name.split('.');
|
|
const loadData = await this.loadNodeFromFile(packageName, fileName, tempPath);
|
|
if (loadData) {
|
|
returnData.push(loadData);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read all credential types
|
|
if (
|
|
packageFile.n8n.hasOwnProperty('credentials') &&
|
|
Array.isArray(packageFile.n8n.credentials)
|
|
) {
|
|
for (filePath of packageFile.n8n.credentials) {
|
|
tempPath = path.join(packagePath, filePath);
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
[fileName, type] = path.parse(filePath).name.split('.');
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
this.loadCredentialsFromFile(fileName, tempPath);
|
|
}
|
|
}
|
|
|
|
return returnData;
|
|
}
|
|
|
|
unloadNodes(installedNodes: InstalledNodes[]): void {
|
|
const nodeTypes = NodeTypes();
|
|
installedNodes.forEach((installedNode) => {
|
|
nodeTypes.removeNodeType(installedNode.type);
|
|
delete this.nodeTypes[installedNode.type];
|
|
});
|
|
}
|
|
|
|
attachNodesToNodeTypes(installedNodes: InstalledNodes[]): void {
|
|
const nodeTypes = NodeTypes();
|
|
installedNodes.forEach((installedNode) => {
|
|
nodeTypes.attachNodeType(
|
|
installedNode.type,
|
|
this.nodeTypes[installedNode.type].type,
|
|
this.nodeTypes[installedNode.type].sourcePath,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
|
|
|
|
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
|
|
if (packagesInformationInstance === undefined) {
|
|
packagesInformationInstance = new LoadNodesAndCredentialsClass();
|
|
}
|
|
|
|
return packagesInformationInstance;
|
|
}
|