2023-01-04 09:16:48 -08:00
|
|
|
import uniq from 'lodash.uniq';
|
2023-03-24 09:04:26 -07:00
|
|
|
import glob from 'fast-glob';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { DirectoryLoader, Types } from 'n8n-core';
|
2019-06-23 03:35:23 -07:00
|
|
|
import {
|
2022-11-23 07:20:28 -08:00
|
|
|
CUSTOM_EXTENSION_ENV,
|
|
|
|
UserSettings,
|
|
|
|
CustomDirectoryLoader,
|
|
|
|
PackageDirectoryLoader,
|
|
|
|
LazyPackageDirectoryLoader,
|
|
|
|
} from 'n8n-core';
|
|
|
|
import type {
|
2023-01-04 09:16:48 -08:00
|
|
|
ICredentialTypes,
|
2021-05-21 21:41:06 -07:00
|
|
|
ILogger,
|
2022-11-23 07:20:28 -08:00
|
|
|
INodesAndCredentials,
|
|
|
|
KnownNodesAndCredentials,
|
2023-02-03 04:14:59 -08:00
|
|
|
INodeTypeDescription,
|
2022-11-23 07:20:28 -08:00
|
|
|
LoadedNodesAndCredentials,
|
2019-06-23 03:35:23 -07:00
|
|
|
} from 'n8n-workflow';
|
2022-11-23 07:20:28 -08:00
|
|
|
import { LoggerProxy, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2022-11-28 08:41:44 -08:00
|
|
|
import { createWriteStream } from 'fs';
|
2023-03-24 09:04:26 -07:00
|
|
|
import { mkdir } from 'fs/promises';
|
2022-04-08 14:32:08 -07:00
|
|
|
import path from 'path';
|
2022-11-09 06:25:00 -08:00
|
|
|
import config from '@/config';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
2022-11-23 07:20:28 -08:00
|
|
|
import { executeCommand } from '@/CommunityNodes/helpers';
|
2023-02-03 04:14:59 -08:00
|
|
|
import {
|
|
|
|
GENERATED_STATIC_DIR,
|
|
|
|
RESPONSE_ERROR_MESSAGES,
|
|
|
|
CUSTOM_API_CALL_KEY,
|
|
|
|
CUSTOM_API_CALL_NAME,
|
2023-02-10 05:59:20 -08:00
|
|
|
inTest,
|
2023-03-24 09:04:26 -07:00
|
|
|
CLI_DIR,
|
2023-02-03 04:14:59 -08:00
|
|
|
} from '@/constants';
|
2022-11-23 07:20:28 -08:00
|
|
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { Service } from 'typedi';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
@Service()
|
|
|
|
export class LoadNodesAndCredentials implements INodesAndCredentials {
|
2022-11-23 07:20:28 -08:00
|
|
|
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
2021-06-17 22:58:26 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
types: Types = { nodes: [], credentials: [] };
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
loaders: Record<string, DirectoryLoader> = {};
|
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
excludeNodes = config.getEnv('nodes.exclude');
|
2021-08-29 11:58:11 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
includeNodes = config.getEnv('nodes.include');
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-01-04 09:16:48 -08:00
|
|
|
credentialTypes: ICredentialTypes;
|
|
|
|
|
2021-05-21 21:41:06 -07:00
|
|
|
logger: ILogger;
|
2021-05-21 20:51:38 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
private downloadFolder: string;
|
|
|
|
|
2019-08-08 11:38:25 -07:00
|
|
|
async init() {
|
2022-07-20 07:24:03 -07:00
|
|
|
// Make sure the imported modules can resolve dependencies fine.
|
2022-08-03 09:10:59 -07:00
|
|
|
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
|
|
process.env.NODE_PATH = module.paths.join(delimiter);
|
2022-11-23 07:20:28 -08:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
// @ts-ignore
|
2022-11-23 07:20:28 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
2023-02-10 05:59:20 -08:00
|
|
|
if (!inTest) module.constructor._initPaths();
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
|
|
|
|
|
|
|
// Load nodes from `n8n-nodes-base` and any other `n8n-nodes-*` package in the main `node_modules`
|
|
|
|
await this.loadNodesFromNodeModules(CLI_DIR);
|
|
|
|
// Load nodes from installed community packages
|
|
|
|
await this.loadNodesFromNodeModules(this.downloadFolder);
|
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
await this.loadNodesFromCustomDirectories();
|
2023-02-08 09:57:43 -08:00
|
|
|
await this.postProcessLoaders();
|
2023-02-03 04:14:59 -08:00
|
|
|
this.injectCustomApiCallOptions();
|
2022-11-23 07:20:28 -08:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
async generateTypesForFrontend() {
|
|
|
|
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
|
|
|
for (const credential of this.types.credentials) {
|
2023-01-04 09:16:48 -08:00
|
|
|
const overwrittenProperties = [];
|
|
|
|
this.credentialTypes
|
|
|
|
.getParentTypes(credential.name)
|
|
|
|
.reverse()
|
|
|
|
.map((name) => credentialsOverwrites[name])
|
|
|
|
.forEach((overwrite) => {
|
|
|
|
if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
|
|
|
|
});
|
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
if (credential.name in credentialsOverwrites) {
|
2023-01-04 09:16:48 -08:00
|
|
|
overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (overwrittenProperties.length) {
|
|
|
|
credential.__overwrittenProperties = uniq(overwrittenProperties);
|
2022-11-23 07:20:28 -08:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
// pre-render all the node and credential types as static json files
|
|
|
|
await mkdir(path.join(GENERATED_STATIC_DIR, 'types'), { recursive: true });
|
|
|
|
|
2022-11-28 08:41:44 -08:00
|
|
|
const writeStaticJSON = async (name: string, data: object[]) => {
|
2022-11-23 07:20:28 -08:00
|
|
|
const filePath = path.join(GENERATED_STATIC_DIR, `types/${name}.json`);
|
2022-11-28 08:41:44 -08:00
|
|
|
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();
|
2022-11-23 07:20:28 -08:00
|
|
|
};
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
await writeStaticJSON('nodes', this.types.nodes);
|
|
|
|
await writeStaticJSON('credentials', this.types.credentials);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
private async loadNodesFromNodeModules(scanDir: string): Promise<void> {
|
|
|
|
const nodeModulesDir = path.join(scanDir, 'node_modules');
|
|
|
|
const globOptions = { cwd: nodeModulesDir, onlyDirectories: true };
|
|
|
|
const installedPackagePaths = [
|
|
|
|
...(await glob('n8n-nodes-*', { ...globOptions, deep: 1 })),
|
|
|
|
...(await glob('@*/n8n-nodes-*', { ...globOptions, deep: 2 })),
|
|
|
|
];
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
for (const packagePath of installedPackagePaths) {
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
2023-03-24 09:04:26 -07:00
|
|
|
await this.runDirectoryLoader(
|
|
|
|
LazyPackageDirectoryLoader,
|
|
|
|
path.join(nodeModulesDir, packagePath),
|
|
|
|
);
|
2022-11-04 09:34:47 -07:00
|
|
|
} catch (error) {
|
|
|
|
ErrorReporter.error(error);
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-01-05 04:28:40 -08:00
|
|
|
getCustomDirectories(): string[] {
|
|
|
|
const customDirectories = [UserSettings.getUserN8nFolderCustomExtensionPath()];
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
2023-01-05 04:28:40 -08:00
|
|
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
|
2022-09-09 09:08:08 -07:00
|
|
|
customDirectories.push(...customExtensionFolders);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
2023-01-05 04:28:40 -08:00
|
|
|
return customDirectories;
|
|
|
|
}
|
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
private async loadNodesFromCustomDirectories(): Promise<void> {
|
2023-01-05 04:28:40 -08:00
|
|
|
for (const directory of this.getCustomDirectories()) {
|
2022-11-23 07:20:28 -08:00
|
|
|
await this.runDirectoryLoader(CustomDirectoryLoader, directory);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
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}` : ''}`;
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
try {
|
|
|
|
await executeCommand(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;
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
const finalNodeUnpackedPath = path.join(this.downloadFolder, 'node_modules', packageName);
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
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 executeCommand(removeCommand);
|
|
|
|
} catch {}
|
|
|
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-02-15 07:09:53 -08:00
|
|
|
if (loader.loadedNodes.length > 0) {
|
2022-07-20 07:24:03 -07:00
|
|
|
// Save info to DB
|
|
|
|
try {
|
2023-03-24 09:04:26 -07:00
|
|
|
const { persistInstalledPackageData, removePackageFromDatabase } = await import(
|
|
|
|
'@/CommunityNodes/packageModel'
|
|
|
|
);
|
|
|
|
if (isUpdate) await removePackageFromDatabase(options.installedPackage);
|
2023-02-15 07:09:53 -08:00
|
|
|
const installedPackage = await persistInstalledPackageData(loader);
|
|
|
|
await this.postProcessLoaders();
|
2022-11-23 07:20:28 -08:00
|
|
|
await this.generateTypesForFrontend();
|
2022-07-20 07:24:03 -07:00
|
|
|
return installedPackage;
|
|
|
|
} catch (error) {
|
2022-11-23 07:20:28 -08:00
|
|
|
LoggerProxy.error('Failed to save installed packages and nodes', {
|
|
|
|
error: error as Error,
|
|
|
|
packageName,
|
|
|
|
});
|
2022-07-20 07:24:03 -07:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Remove this package since it contains no loadable nodes
|
|
|
|
const removeCommand = `npm remove ${packageName}`;
|
|
|
|
try {
|
|
|
|
await executeCommand(removeCommand);
|
2023-03-03 09:18:49 -08:00
|
|
|
} catch {}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
|
|
|
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
|
|
|
return this.installOrUpdateNpmModule(packageName, { version });
|
|
|
|
}
|
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
|
|
|
const command = `npm remove ${packageName}`;
|
|
|
|
|
|
|
|
await executeCommand(command);
|
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
const { removePackageFromDatabase } = await import('@/CommunityNodes/packageModel');
|
2022-11-23 07:20:28 -08:00
|
|
|
await removePackageFromDatabase(installedPackage);
|
|
|
|
|
2023-02-15 07:09:53 -08:00
|
|
|
if (packageName in this.loaders) {
|
|
|
|
this.loaders[packageName].reset();
|
|
|
|
delete this.loaders[packageName];
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-02-15 07:09:53 -08:00
|
|
|
await this.postProcessLoaders();
|
|
|
|
await this.generateTypesForFrontend();
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async updateNpmModule(
|
|
|
|
packageName: string,
|
|
|
|
installedPackage: InstalledPackages,
|
|
|
|
): Promise<InstalledPackages> {
|
2023-03-24 09:04:26 -07:00
|
|
|
return this.installOrUpdateNpmModule(packageName, { installedPackage });
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2023-02-03 04:14:59 -08:00
|
|
|
/**
|
|
|
|
* Whether any of the node's credential types may be used to
|
|
|
|
* make a request from a node other than itself.
|
|
|
|
*/
|
|
|
|
private supportsProxyAuth(description: INodeTypeDescription) {
|
|
|
|
if (!description.credentials) return false;
|
|
|
|
|
|
|
|
return description.credentials.some(({ name }) => {
|
|
|
|
const credType = this.types.credentials.find((t) => t.name === name);
|
|
|
|
if (!credType) {
|
|
|
|
LoggerProxy.warn(
|
|
|
|
`Failed to load Custom API options for the node "${description.name}": Unknown credential name "${name}"`,
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (credType.authenticate !== undefined) return true;
|
|
|
|
|
|
|
|
return (
|
|
|
|
Array.isArray(credType.extends) &&
|
|
|
|
credType.extends.some((parentType) =>
|
|
|
|
['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inject a `Custom API Call` option into `resource` and `operation`
|
|
|
|
* parameters in a latest-version node that supports proxy auth.
|
|
|
|
*/
|
|
|
|
private injectCustomApiCallOptions() {
|
|
|
|
this.types.nodes.forEach((node: INodeTypeDescription) => {
|
|
|
|
const isLatestVersion =
|
|
|
|
node.defaultVersion === undefined || node.defaultVersion === node.version;
|
|
|
|
|
|
|
|
if (isLatestVersion) {
|
|
|
|
if (!this.supportsProxyAuth(node)) return;
|
|
|
|
|
|
|
|
node.properties.forEach((p) => {
|
|
|
|
if (
|
|
|
|
['resource', 'operation'].includes(p.name) &&
|
|
|
|
Array.isArray(p.options) &&
|
|
|
|
p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME
|
|
|
|
) {
|
|
|
|
p.options.push({
|
|
|
|
name: CUSTOM_API_CALL_NAME,
|
|
|
|
value: CUSTOM_API_CALL_KEY,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-17 22:58:26 -07:00
|
|
|
/**
|
2022-11-23 07:20:28 -08:00
|
|
|
* Run a loader of source files of nodes and credentials in a directory.
|
2021-06-17 22:58:26 -07:00
|
|
|
*/
|
2022-11-23 07:20:28 -08:00
|
|
|
private async runDirectoryLoader<T extends DirectoryLoader>(
|
|
|
|
constructor: new (...args: ConstructorParameters<typeof DirectoryLoader>) => T,
|
|
|
|
dir: string,
|
|
|
|
) {
|
|
|
|
const loader = new constructor(dir, this.excludeNodes, this.includeNodes);
|
|
|
|
await loader.loadAll();
|
2023-02-15 07:09:53 -08:00
|
|
|
this.loaders[loader.packageName] = loader;
|
2023-02-08 09:57:43 -08:00
|
|
|
return loader;
|
|
|
|
}
|
2022-11-23 07:20:28 -08:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
async postProcessLoaders() {
|
2023-02-15 07:09:53 -08:00
|
|
|
this.known = { nodes: {}, credentials: {} };
|
|
|
|
this.loaded = { nodes: {}, credentials: {} };
|
|
|
|
this.types = { nodes: [], credentials: [] };
|
|
|
|
|
|
|
|
for (const loader of Object.values(this.loaders)) {
|
2023-02-08 09:57:43 -08:00
|
|
|
// list of node & credential types that will be sent to the frontend
|
2023-02-15 07:09:53 -08:00
|
|
|
const { types, directory } = loader;
|
2023-02-08 09:57:43 -08:00
|
|
|
this.types.nodes = this.types.nodes.concat(types.nodes);
|
|
|
|
this.types.credentials = this.types.credentials.concat(types.credentials);
|
|
|
|
|
|
|
|
// Nodes and credentials that have been loaded immediately
|
|
|
|
for (const nodeTypeName in loader.nodeTypes) {
|
|
|
|
this.loaded.nodes[nodeTypeName] = loader.nodeTypes[nodeTypeName];
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
for (const credentialTypeName in loader.credentialTypes) {
|
|
|
|
this.loaded.credentials[credentialTypeName] = loader.credentialTypes[credentialTypeName];
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
// Nodes and credentials that will be lazy loaded
|
|
|
|
if (loader instanceof PackageDirectoryLoader) {
|
|
|
|
const { packageName, known } = loader;
|
2022-11-23 07:20:28 -08:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
for (const type in known.nodes) {
|
|
|
|
const { className, sourcePath } = known.nodes[type];
|
|
|
|
this.known.nodes[type] = {
|
|
|
|
className,
|
2023-02-15 07:09:53 -08:00
|
|
|
sourcePath: path.join(directory, sourcePath),
|
2023-02-08 09:57:43 -08:00
|
|
|
};
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
for (const type in known.credentials) {
|
|
|
|
const { className, sourcePath, nodesToTestWith } = known.credentials[type];
|
|
|
|
this.known.credentials[type] = {
|
|
|
|
className,
|
2023-02-15 07:09:53 -08:00
|
|
|
sourcePath: path.join(directory, sourcePath),
|
2023-02-08 09:57:43 -08:00
|
|
|
nodesToTestWith: nodesToTestWith?.map((nodeName) => `${packageName}.${nodeName}`),
|
|
|
|
};
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|