mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(cli): Load all nodes and credentials code in isolation - N8N-4362 (#3906)
[N8N-4362] Load all nodes and credentials code in isolation Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
committed by
GitHub
parent
9267e8fb12
commit
b450e977a3
@@ -4,7 +4,7 @@
|
|||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
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 { createContext, Script } from 'vm';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow';
|
import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow';
|
||||||
@@ -235,3 +235,10 @@ export const isClientError = (error: Error): boolean => {
|
|||||||
export function isNpmError(error: unknown): error is { code: number; stdout: string } {
|
export function isNpmError(error: unknown): error is { code: number; stdout: string } {
|
||||||
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const context = createContext({ require });
|
||||||
|
export const loadClassInIsolation = (filePath: string, className: string) => {
|
||||||
|
const script = new Script(`new (require('${filePath}').${className})()`);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return script.runInContext(context);
|
||||||
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import config from '../config';
|
|||||||
import { NodeTypes } from '.';
|
import { NodeTypes } from '.';
|
||||||
import { InstalledPackages } from './databases/entities/InstalledPackages';
|
import { InstalledPackages } from './databases/entities/InstalledPackages';
|
||||||
import { InstalledNodes } from './databases/entities/InstalledNodes';
|
import { InstalledNodes } from './databases/entities/InstalledNodes';
|
||||||
import { executeCommand } from './CommunityNodes/helpers';
|
import { executeCommand, loadClassInIsolation } from './CommunityNodes/helpers';
|
||||||
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
import {
|
import {
|
||||||
persistInstalledPackageData,
|
persistInstalledPackageData,
|
||||||
@@ -46,6 +46,14 @@ import {
|
|||||||
|
|
||||||
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
|
||||||
|
function toJSON() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
authenticate: typeof this.authenticate === 'function' ? {} : this.authenticate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class LoadNodesAndCredentialsClass {
|
class LoadNodesAndCredentialsClass {
|
||||||
nodeTypes: INodeTypeData = {};
|
nodeTypes: INodeTypeData = {};
|
||||||
|
|
||||||
@@ -104,10 +112,8 @@ class LoadNodesAndCredentialsClass {
|
|||||||
await fsAccess(checkPath);
|
await fsAccess(checkPath);
|
||||||
// Folder exists, so use it.
|
// Folder exists, so use it.
|
||||||
return path.dirname(checkPath);
|
return path.dirname(checkPath);
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
// Folder does not exist so get next one
|
// Folder does not exist so get next one
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('Could not find "node_modules" folder!');
|
throw new Error('Could not find "node_modules" folder!');
|
||||||
@@ -144,8 +150,7 @@ class LoadNodesAndCredentialsClass {
|
|||||||
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV]!.split(';');
|
||||||
// eslint-disable-next-line prefer-spread
|
customDirectories.push(...customExtensionFolders);
|
||||||
customDirectories.push.apply(customDirectories, customExtensionFolders);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const directory of customDirectories) {
|
for (const directory of customDirectories) {
|
||||||
@@ -192,26 +197,16 @@ class LoadNodesAndCredentialsClass {
|
|||||||
* @param {string} filePath The file to read credentials from
|
* @param {string} filePath The file to read credentials from
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
loadCredentialsFromFile(credentialName: string, filePath: string): void {
|
||||||
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
||||||
const tempModule = require(filePath);
|
|
||||||
|
|
||||||
let tempCredential: ICredentialType;
|
let tempCredential: ICredentialType;
|
||||||
try {
|
try {
|
||||||
|
tempCredential = loadClassInIsolation(filePath, credentialName);
|
||||||
|
|
||||||
// Add serializer method "toJSON" to the class so that authenticate method (if defined)
|
// 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.
|
// 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
|
// The authenticate property is used by the client to decide whether or not to
|
||||||
// include the credential type in the predefined credentials (HTTP node)
|
// include the credential type in the predefined credentials (HTTP node)
|
||||||
// eslint-disable-next-line func-names
|
Object.assign(tempCredential, { toJSON });
|
||||||
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 (tempCredential.icon && tempCredential.icon.startsWith('file:')) {
|
||||||
// If a file icon gets used add the full path
|
// If a file icon gets used add the full path
|
||||||
@@ -353,19 +348,16 @@ class LoadNodesAndCredentialsClass {
|
|||||||
* @param {string} filePath The file to read node from
|
* @param {string} filePath The file to read node from
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadNodeFromFile(
|
loadNodeFromFile(
|
||||||
packageName: string,
|
packageName: string,
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
): Promise<INodeTypeNameVersion | undefined> {
|
): INodeTypeNameVersion | undefined {
|
||||||
let tempNode: INodeType | INodeVersionedType;
|
let tempNode: INodeType | INodeVersionedType;
|
||||||
let fullNodeName: string;
|
|
||||||
let nodeVersion = 1;
|
let nodeVersion = 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
tempNode = loadClassInIsolation(filePath, nodeName);
|
||||||
const tempModule = require(filePath);
|
|
||||||
tempNode = new tempModule[nodeName]();
|
|
||||||
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line no-console, @typescript-eslint/restrict-template-expressions
|
||||||
@@ -373,8 +365,7 @@ class LoadNodesAndCredentialsClass {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-const
|
const fullNodeName = `${packageName}.${tempNode.description.name}`;
|
||||||
fullNodeName = `${packageName}.${tempNode.description.name}`;
|
|
||||||
tempNode.description.name = fullNodeName;
|
tempNode.description.name = fullNodeName;
|
||||||
|
|
||||||
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
|
if (tempNode.description.icon !== undefined && tempNode.description.icon.startsWith('file:')) {
|
||||||
@@ -385,13 +376,6 @@ class LoadNodesAndCredentialsClass {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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')) {
|
if (tempNode.hasOwnProperty('nodeVersions')) {
|
||||||
const versionedNodeType = (tempNode as INodeVersionedType).getNodeType();
|
const versionedNodeType = (tempNode as INodeVersionedType).getNodeType();
|
||||||
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
|
this.addCodex({ node: versionedNodeType, filePath, isCustom: packageName === 'CUSTOM' });
|
||||||
@@ -491,8 +475,7 @@ class LoadNodesAndCredentialsClass {
|
|||||||
|
|
||||||
node.description.codex = codex;
|
node.description.codex = codex;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
this.logger.debug(`No codex available for: ${filePath.split('/').pop() ?? ''}`);
|
||||||
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
|
|
||||||
|
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
node.description.codex = {
|
node.description.codex = {
|
||||||
@@ -512,22 +495,15 @@ class LoadNodesAndCredentialsClass {
|
|||||||
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
||||||
const files = await glob(path.join(directory, '**/*.@(node|credentials).js'));
|
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) {
|
for (const filePath of files) {
|
||||||
[fileName, type] = path.parse(filePath).name.split('.');
|
const [fileName, type] = path.parse(filePath).name.split('.');
|
||||||
|
|
||||||
if (type === 'node') {
|
if (type === 'node') {
|
||||||
loadPromises.push(this.loadNodeFromFile(setPackageName, fileName, filePath));
|
this.loadNodeFromFile(setPackageName, fileName, filePath);
|
||||||
} else if (type === 'credentials') {
|
} else if (type === 'credentials') {
|
||||||
loadPromises.push(this.loadCredentialsFromFile(fileName, filePath));
|
this.loadCredentialsFromFile(fileName, filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(loadPromises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> {
|
async readPackageJson(packagePath: string): Promise<IN8nNodePackageJson> {
|
||||||
@@ -545,26 +521,20 @@ class LoadNodesAndCredentialsClass {
|
|||||||
async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> {
|
async loadDataFromPackage(packagePath: string): Promise<INodeTypeNameVersion[]> {
|
||||||
// Get the absolute path of the package
|
// Get the absolute path of the package
|
||||||
const packageFile = await this.readPackageJson(packagePath);
|
const packageFile = await this.readPackageJson(packagePath);
|
||||||
// if (!packageFile.hasOwnProperty('n8n')) {
|
|
||||||
if (!packageFile.n8n) {
|
if (!packageFile.n8n) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageName = packageFile.name;
|
const packageName = packageFile.name;
|
||||||
|
const { nodes, credentials } = packageFile.n8n;
|
||||||
let tempPath: string;
|
|
||||||
let filePath: string;
|
|
||||||
|
|
||||||
const returnData: INodeTypeNameVersion[] = [];
|
const returnData: INodeTypeNameVersion[] = [];
|
||||||
|
|
||||||
// Read all node types
|
// Read all node types
|
||||||
let fileName: string;
|
if (Array.isArray(nodes)) {
|
||||||
let type: string;
|
for (const filePath of nodes) {
|
||||||
if (packageFile.n8n.hasOwnProperty('nodes') && Array.isArray(packageFile.n8n.nodes)) {
|
const tempPath = path.join(packagePath, filePath);
|
||||||
for (filePath of packageFile.n8n.nodes) {
|
const [fileName] = path.parse(filePath).name.split('.');
|
||||||
tempPath = path.join(packagePath, filePath);
|
const loadData = this.loadNodeFromFile(packageName, fileName, tempPath);
|
||||||
[fileName, type] = path.parse(filePath).name.split('.');
|
|
||||||
const loadData = await this.loadNodeFromFile(packageName, fileName, tempPath);
|
|
||||||
if (loadData) {
|
if (loadData) {
|
||||||
returnData.push(loadData);
|
returnData.push(loadData);
|
||||||
}
|
}
|
||||||
@@ -572,15 +542,10 @@ class LoadNodesAndCredentialsClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read all credential types
|
// Read all credential types
|
||||||
if (
|
if (Array.isArray(credentials)) {
|
||||||
packageFile.n8n.hasOwnProperty('credentials') &&
|
for (const filePath of credentials) {
|
||||||
Array.isArray(packageFile.n8n.credentials)
|
const tempPath = path.join(packagePath, filePath);
|
||||||
) {
|
const [fileName] = path.parse(filePath).name.split('.');
|
||||||
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);
|
this.loadCredentialsFromFile(fileName, tempPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { getLogger } from './Logger';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { InternalHooksManager } from './InternalHooksManager';
|
import { InternalHooksManager } from './InternalHooksManager';
|
||||||
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
|
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
|
||||||
|
import { loadClassInIsolation } from './CommunityNodes/helpers';
|
||||||
|
|
||||||
export class WorkflowRunnerProcess {
|
export class WorkflowRunnerProcess {
|
||||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||||
@@ -92,41 +93,30 @@ export class WorkflowRunnerProcess {
|
|||||||
workflowId: this.data.workflowData.id,
|
workflowId: this.data.workflowData.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
let className: string;
|
|
||||||
let tempNode: INodeType;
|
|
||||||
let tempCredential: ICredentialType;
|
|
||||||
let filePath: string;
|
|
||||||
|
|
||||||
this.startedAt = new Date();
|
this.startedAt = new Date();
|
||||||
|
|
||||||
// Load the required nodes
|
// Load the required nodes
|
||||||
const nodeTypesData: INodeTypeData = {};
|
const nodeTypesData: INodeTypeData = {};
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
|
for (const nodeTypeName of Object.keys(this.data.nodeTypeData)) {
|
||||||
className = this.data.nodeTypeData[nodeTypeName].className;
|
let tempNode: INodeType;
|
||||||
|
const { className, sourcePath } = this.data.nodeTypeData[nodeTypeName];
|
||||||
filePath = this.data.nodeTypeData[nodeTypeName].sourcePath;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
||||||
const tempModule = require(filePath);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
const nodeObject = loadClassInIsolation(sourcePath, className);
|
||||||
const nodeObject = new tempModule[className]();
|
|
||||||
if (nodeObject.getNodeType !== undefined) {
|
if (nodeObject.getNodeType !== undefined) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
tempNode = nodeObject.getNodeType();
|
tempNode = nodeObject.getNodeType();
|
||||||
} else {
|
} else {
|
||||||
tempNode = nodeObject;
|
tempNode = nodeObject;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
||||||
tempNode = new tempModule[className]() as INodeType;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error loading node "${nodeTypeName}" from: "${filePath}"`);
|
throw new Error(`Error loading node "${nodeTypeName}" from: "${sourcePath}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeTypesData[nodeTypeName] = {
|
nodeTypesData[nodeTypeName] = {
|
||||||
type: tempNode,
|
type: tempNode,
|
||||||
sourcePath: filePath,
|
sourcePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,22 +127,18 @@ export class WorkflowRunnerProcess {
|
|||||||
const credentialsTypeData: ICredentialTypeData = {};
|
const credentialsTypeData: ICredentialTypeData = {};
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) {
|
for (const credentialTypeName of Object.keys(this.data.credentialsTypeData)) {
|
||||||
className = this.data.credentialsTypeData[credentialTypeName].className;
|
let tempCredential: ICredentialType;
|
||||||
|
const { className, sourcePath } = this.data.credentialsTypeData[credentialTypeName];
|
||||||
filePath = this.data.credentialsTypeData[credentialTypeName].sourcePath;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, import/no-dynamic-require, global-require, @typescript-eslint/no-var-requires
|
|
||||||
const tempModule = require(filePath);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
tempCredential = loadClassInIsolation(sourcePath, className);
|
||||||
tempCredential = new tempModule[className]() as ICredentialType;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error loading credential "${credentialTypeName}" from: "${filePath}"`);
|
throw new Error(`Error loading credential "${credentialTypeName}" from: "${sourcePath}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
credentialsTypeData[credentialTypeName] = {
|
credentialsTypeData[credentialTypeName] = {
|
||||||
type: tempCredential,
|
type: tempCredential,
|
||||||
sourcePath: filePath,
|
sourcePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user