Skip to content

Commit

Permalink
Created community node helpers and removed packages taht do not conta…
Browse files Browse the repository at this point in the history
…in nodes
  • Loading branch information
krynble committed Apr 25, 2022
1 parent 58d6585 commit 55ebf17
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 61 deletions.
65 changes: 65 additions & 0 deletions packages/cli/src/CommunityNodes/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { promisify } from 'util';
import { exec } from 'child_process';
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';

import { UserSettings } from 'n8n-core';
import { NODE_PACKAGE_PREFIX, RESPONSE_ERROR_MESSAGES } from '../constants';
import { IParsedNpmPackageName } from '../Interfaces';

const execAsync = promisify(exec);

export const parsePackageName = (originalString: string | undefined): IParsedNpmPackageName => {
if (!originalString) {
throw new Error('Package name was not provided');
}

const scope = originalString.includes('/') ? originalString.split('/')[0] : undefined;

const packageNameWithoutScope = scope ? originalString.replace(scope, '') : originalString;

if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) {
throw new Error('Package name is not valid');
}

const version = packageNameWithoutScope.includes('@')
? packageNameWithoutScope.split('@')[1]
: undefined;

const packageName = version ? originalString.replace(`@${version}`, '') : originalString;

return {
packageName,
scope,
version,
originalString,
};
};

export const executeCommand = async (command: string): Promise<void> => {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
// Make sure the node-download folder exists
try {
await fsAccess(downloadFolder);
// eslint-disable-next-line no-empty
} catch (error) {
await fsMkdir(downloadFolder);
}
const execOptions = {
cwd: downloadFolder,
env: {
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
},
};

try {
await execAsync(command, execOptions);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
if (error.message.includes('404 Not Found')) {
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
}
throw error;
}
};
7 changes: 7 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,10 @@ export interface IWorkflowExecuteProcess {
}

export type WhereClause = Record<string, { id: string }>;

export interface IParsedNpmPackageName {
packageName: string;
originalString: string;
scope?: string;
version?: string;
}
79 changes: 23 additions & 56 deletions packages/cli/src/LoadNodesAndCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,20 @@ import {

import {
access as fsAccess,
mkdir as fsMkdir,
readdir as fsReaddir,
readFile as fsReadFile,
stat as fsStat,
} from 'fs/promises';
import glob from 'fast-glob';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { IN8nNodePackageJson } from './Interfaces';
import { getLogger } from './Logger';
import config from '../config';
import { Db } from '.';
import { InstalledPackages } from './databases/entities/InstalledPackages';
import { InstalledNodes } from './databases/entities/InstalledNodes';

const execAsync = promisify(exec);
import { executeCommand, parsePackageName } from './CommunityNodes/helpers';
import { RESPONSE_ERROR_MESSAGES } from './constants';

const CUSTOM_NODES_CATEGORY = 'Custom Nodes';

Expand Down Expand Up @@ -205,42 +202,23 @@ class LoadNodesAndCredentialsClass {
}

async loadNpmModule(packageName: string): Promise<INodeTypeNameVersion[]> {
const parsedPackaeName = parsePackageName(packageName);
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();

// Make sure the node-download folder exists
try {
await fsAccess(downloadFolder);
// eslint-disable-next-line no-empty
} catch (error) {
await fsMkdir(downloadFolder);
}

const command = `npm install ${packageName}`;
const execOptions = {
cwd: downloadFolder,
env: {
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
},
};

try {
await execAsync(command, execOptions);
await executeCommand(command);
} catch (error) {
if (error.message.includes('404 Not Found')) {
if (error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`);
}
throw error;
}

const packageNameWithoutVersion = packageName.includes('@')
? packageName.split('@')[0]
: packageName;

const finalNodeUnpackedPath = path.join(
downloadFolder,
'node_modules',
packageNameWithoutVersion,
parsedPackaeName.packageName,
);

const loadedNodes = await this.loadDataFromPackage(finalNodeUnpackedPath);
Expand All @@ -254,7 +232,7 @@ class LoadNodesAndCredentialsClass {
const promises = [];

const installedPackage = Object.assign(new InstalledPackages(), {
packageName: packageNameWithoutVersion,
packageName: parsedPackaeName.packageName,
installedVersion: packageFile.version,
});
await transactionManager.save<InstalledPackages>(installedPackage);
Expand All @@ -265,7 +243,7 @@ class LoadNodesAndCredentialsClass {
name: loadedNode.name,
type: loadedNode.name,
latestVersion: loadedNode.version,
package: packageNameWithoutVersion,
package: parsedPackaeName.packageName,
});
return transactionManager.save<InstalledNodes>(installedNode);
}),
Expand All @@ -279,31 +257,21 @@ class LoadNodesAndCredentialsClass {
}
} else {
// Remove this package since it contains no loadable nodes
const removeCommand = `npm remove ${packageName}`;
try {
await executeCommand(removeCommand);
} catch (error) {
// Do nothing
}
}

return loadedNodes;
}

async removeNpmModule(packageName: string, installedNodes: InstalledNodes[]): Promise<void> {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();

const command = `npm remove ${packageName}`;
const execOptions = {
cwd: downloadFolder,
env: {
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
},
};

try {
await execAsync(command, execOptions);
} catch (error) {
if (error.message.includes('404 Not Found')) {
throw new Error(`The npm package "${packageName}" could not be found.`);
}
throw error;
}
await executeCommand(command);

installedNodes.forEach((installedNode) => {
delete this.nodeTypes[installedNode.name];
Expand All @@ -317,18 +285,11 @@ class LoadNodesAndCredentialsClass {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();

const command = `npm update ${packageName}`;
const execOptions = {
cwd: downloadFolder,
env: {
NODE_PATH: process.env.NODE_PATH,
PATH: process.env.PATH,
},
};

try {
await execAsync(command, execOptions);
await executeCommand(command);
} catch (error) {
if (error.message.includes('404 Not Found')) {
if (error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`);
}
throw error;
Expand Down Expand Up @@ -384,6 +345,12 @@ class LoadNodesAndCredentialsClass {
}
} else {
// Remove this package since it contains no loadable nodes
const removeCommand = `npm remove ${packageName}`;
try {
await executeCommand(removeCommand);
} catch (error) {
// Do nothing
}
}

return loadedNodes;
Expand Down
39 changes: 34 additions & 5 deletions packages/cli/src/api/nodes.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getLogger } from '../Logger';

import { Db, ResponseHelper, LoadNodesAndCredentials, Push } from '..';
import { NodeRequest } from '../requests';
import { RESPONSE_ERROR_MESSAGES } from '../constants';

export const nodesController = express.Router();

Expand All @@ -26,7 +27,11 @@ nodesController.post(
ResponseHelper.send(async (req: NodeRequest.Post) => {
const { name } = req.body;
if (!name) {
throw new ResponseHelper.ResponseError(`The parameter "name" is missing!`, undefined, 400);
throw new ResponseHelper.ResponseError(
RESPONSE_ERROR_MESSAGES.PACKAGE_NAME_NOT_PROVIDED,
undefined,
400,
);
}

try {
Expand Down Expand Up @@ -69,8 +74,13 @@ nodesController.delete(
ResponseHelper.send(async (req: NodeRequest.Delete) => {
const { name } = req.body;
if (!name) {
throw new ResponseHelper.ResponseError(`The parameter "name" is missing!`, undefined, 400);
throw new ResponseHelper.ResponseError(
RESPONSE_ERROR_MESSAGES.PACKAGE_NAME_NOT_PROVIDED,
undefined,
400,
);
}

const installedPackage = await Db.collections.InstalledPackages.findOne({
where: {
packageName: name,
Expand All @@ -79,7 +89,11 @@ nodesController.delete(
});

if (!installedPackage) {
throw new ResponseHelper.ResponseError('Package is not installed', undefined, 400);
throw new ResponseHelper.ResponseError(
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_INSTALLED,
undefined,
400,
);
}

try {
Expand Down Expand Up @@ -112,7 +126,11 @@ nodesController.patch(
ResponseHelper.send(async (req: NodeRequest.Update) => {
const { name } = req.body;
if (!name) {
throw new ResponseHelper.ResponseError(`The parameter "name" is missing!`, undefined, 400);
throw new ResponseHelper.ResponseError(
RESPONSE_ERROR_MESSAGES.PACKAGE_NAME_NOT_PROVIDED,
undefined,
400,
);
}
const installedPackage = await Db.collections.InstalledPackages.findOne({
where: {
Expand All @@ -122,7 +140,11 @@ nodesController.patch(
});

if (!installedPackage) {
throw new ResponseHelper.ResponseError('Package is not installed', undefined, 400);
throw new ResponseHelper.ResponseError(
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_INSTALLED,
undefined,
400,
);
}

try {
Expand All @@ -145,6 +167,13 @@ nodesController.patch(
pushInstance.send('reloadNodeType', nodeData);
});
} catch (error) {
installedPackage.installedNodes.forEach((installedNode) => {
const pushInstance = Push.getInstance();
pushInstance.send('removeNodeType', {
name: installedNode.type,
version: installedNode.latestVersion,
});
});
throw new ResponseHelper.ResponseError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`Error updating package "${name}": ${error.message}`,
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@

import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES } from 'n8n-core';

export const NODE_PACKAGE_PREFIX = 'n8n-nodes-';

export const RESPONSE_ERROR_MESSAGES = {
NO_CREDENTIAL: 'Credential not found',
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
PACKAGE_NAME_NOT_PROVIDED: 'Package name is required',
PACKAGE_NAME_NOT_VALID: `Package name is not valid - it must start with "${NODE_PACKAGE_PREFIX}"`,
PACKAGE_NOT_INSTALLED: 'This package is not installed - you must install it first',
PACKAGE_NOT_FOUND: 'Failed installing package: package was not found',
};

export const AUTH_COOKIE_NAME = 'n8n-auth';

0 comments on commit 55ebf17

Please sign in to comment.