mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
refactor(core): Improve community node repo code (#3767)
* 📘 Tighten `NodeRequest` * :blue: Add `AuthAgent` type * ⚡ Add constants * 📘 Namespace npm types * 🧪 Set up `createAuthAgent` * 🧪 Refactor helpers tests * 🧪 Refactor endpoints tests * ⚡ Refactor CNR helpers * ⚡ Return promises in `packageModel` * ⚡ Refactor endpoints * ✏️ Restore naming * ➕ Expose dependency `jest-mock` * 📦 Update `package-lock.json` * 📦 Update `package-lock.json` * 👕 Fix lint * 🚚 Rename namespace * 🔥 Remove outdated comment * 🐛 Fix `Promise` comparison * ⏪ Undo `ResponseHelper` change * ✏️ Document `ResponseError` * 🎨 Fix formatting
This commit is contained in:
parent
ad8d662976
commit
7e578b7f4d
156
package-lock.json
generated
156
package-lock.json
generated
|
@ -171,6 +171,7 @@
|
|||
"iso-639-1": "^2.1.3",
|
||||
"jest": "^27.4.7",
|
||||
"jest-environment-jsdom": "^27.5.1",
|
||||
"jest-mock": "^28.1.3",
|
||||
"jmespath": "^0.16.0",
|
||||
"jquery": "^3.4.1",
|
||||
"jsdom": "19.0.0",
|
||||
|
@ -3496,6 +3497,18 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment/node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/environment/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
@ -3561,6 +3574,18 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/fake-timers/node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jest/fake-timers/node_modules/jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -39769,6 +39794,18 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -39984,6 +40021,18 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-environment-node/node_modules/jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -40378,36 +40427,37 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz",
|
||||
"integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@jest/types": "^28.1.3",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-mock/node_modules/@jest/types": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
|
||||
"integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==",
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
|
||||
"integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^28.1.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"@types/yargs": "^17.0.8",
|
||||
"chalk": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-mock/node_modules/@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
"integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==",
|
||||
"dependencies": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
|
@ -41040,6 +41090,18 @@
|
|||
"fsevents": "^2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-runtime/node_modules/jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"dependencies": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-runtime/node_modules/jest-regex-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
|
||||
|
@ -64044,6 +64106,15 @@
|
|||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
@ -64096,6 +64167,15 @@
|
|||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -92256,6 +92336,15 @@
|
|||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -93225,6 +93314,15 @@
|
|||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz",
|
||||
|
@ -93533,30 +93631,31 @@
|
|||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz",
|
||||
"integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@jest/types": "^28.1.3",
|
||||
"@types/node": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz",
|
||||
"integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==",
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
|
||||
"integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
|
||||
"requires": {
|
||||
"@jest/schemas": "^28.1.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"@types/yargs": "^17.0.8",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
|
||||
"integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==",
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
|
@ -94046,6 +94145,15 @@
|
|||
"walker": "^1.0.7"
|
||||
}
|
||||
},
|
||||
"jest-mock": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz",
|
||||
"integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==",
|
||||
"requires": {
|
||||
"@jest/types": "^27.5.1",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz",
|
||||
|
|
|
@ -88,6 +88,7 @@
|
|||
"@types/validator": "^13.7.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"jest": "^27.4.7",
|
||||
"jest-mock": "^28.1.3",
|
||||
"nodemon": "^2.0.2",
|
||||
"run-script-os": "^1.0.7",
|
||||
"supertest": "^6.2.2",
|
||||
|
@ -96,9 +97,9 @@
|
|||
"typescript": "~4.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/core": "^1.9.3",
|
||||
"@apidevtools/swagger-cli": "4.0.0",
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/core": "^1.9.3",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
|
|
|
@ -1,71 +1,89 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable import/no-cycle */
|
||||
/* 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 axios from 'axios';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow';
|
||||
import axios from 'axios';
|
||||
|
||||
import {
|
||||
NODE_PACKAGE_PREFIX,
|
||||
NPM_COMMAND_TOKENS,
|
||||
NPM_PACKAGE_STATUS_GOOD,
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
UNKNOWN_FAILURE_REASON,
|
||||
} from '../constants';
|
||||
import { NpmPackageStatusCheck, NpmUpdatesAvailable, ParsedNpmPackageName } from '../Interfaces';
|
||||
import { InstalledPackages } from '../databases/entities/InstalledPackages';
|
||||
import config from '../../config';
|
||||
|
||||
import type { CommunityPackages } from '../Interfaces';
|
||||
|
||||
const {
|
||||
PACKAGE_NAME_NOT_PROVIDED,
|
||||
DISK_IS_FULL,
|
||||
PACKAGE_FAILED_TO_INSTALL,
|
||||
PACKAGE_VERSION_NOT_FOUND,
|
||||
PACKAGE_DOES_NOT_CONTAIN_NODES,
|
||||
PACKAGE_NOT_FOUND,
|
||||
} = RESPONSE_ERROR_MESSAGES;
|
||||
|
||||
const {
|
||||
NPM_PACKAGE_NOT_FOUND_ERROR,
|
||||
NPM_NO_VERSION_AVAILABLE,
|
||||
NPM_DISK_NO_SPACE,
|
||||
NPM_DISK_INSUFFICIENT_SPACE,
|
||||
NPM_PACKAGE_VERSION_NOT_FOUND_ERROR,
|
||||
} = NPM_COMMAND_TOKENS;
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const parsePackageName = (originalString: string | undefined): ParsedNpmPackageName => {
|
||||
if (!originalString) {
|
||||
throw new Error('Package name was not provided');
|
||||
}
|
||||
const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
|
||||
|
||||
if (new RegExp(/[^0-9a-z@\-./]/).test(originalString)) {
|
||||
// Prevent any strings that are not valid npm package names or
|
||||
// could indicate malicous commands
|
||||
export const parseNpmPackageName = (rawString?: string): CommunityPackages.ParsedPackageName => {
|
||||
if (!rawString) throw new Error(PACKAGE_NAME_NOT_PROVIDED);
|
||||
|
||||
if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString))
|
||||
throw new Error('Package name must be a single word');
|
||||
}
|
||||
|
||||
const scope = originalString.includes('/') ? originalString.split('/')[0] : undefined;
|
||||
const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined;
|
||||
|
||||
const packageNameWithoutScope = scope ? originalString.replace(`${scope}/`, '') : originalString;
|
||||
const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString;
|
||||
|
||||
if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) {
|
||||
throw new Error('Package name must start with n8n-nodes-');
|
||||
throw new Error(`Package name must start with ${NODE_PACKAGE_PREFIX}`);
|
||||
}
|
||||
|
||||
const version = packageNameWithoutScope.includes('@')
|
||||
? packageNameWithoutScope.split('@')[1]
|
||||
: undefined;
|
||||
|
||||
const packageName = version ? originalString.replace(`@${version}`, '') : originalString;
|
||||
const packageName = version ? rawString.replace(`@${version}`, '') : rawString;
|
||||
|
||||
return {
|
||||
packageName,
|
||||
scope,
|
||||
version,
|
||||
originalString,
|
||||
rawString,
|
||||
};
|
||||
};
|
||||
|
||||
export const sanitizeNpmPackageName = parseNpmPackageName;
|
||||
|
||||
export const executeCommand = async (
|
||||
command: string,
|
||||
options?: {
|
||||
doNotHandleError?: boolean;
|
||||
},
|
||||
options?: { doNotHandleError?: boolean },
|
||||
): Promise<string> => {
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
||||
// Make sure the node-download folder exists
|
||||
|
||||
try {
|
||||
await fsAccess(downloadFolder);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
await fsMkdir(downloadFolder);
|
||||
}
|
||||
|
||||
const execOptions = {
|
||||
cwd: downloadFolder,
|
||||
env: {
|
||||
|
@ -76,57 +94,48 @@ export const executeCommand = async (
|
|||
|
||||
try {
|
||||
const commandResult = await execAsync(command, execOptions);
|
||||
|
||||
return commandResult.stdout;
|
||||
} catch (error) {
|
||||
if (options?.doNotHandleError) {
|
||||
throw error;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const errorMessage = error.message as string;
|
||||
if (options?.doNotHandleError) throw error;
|
||||
|
||||
if (
|
||||
errorMessage.includes(NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR) ||
|
||||
errorMessage.includes(NPM_COMMAND_TOKENS.NPM_NO_VERSION_AVAILABLE)
|
||||
) {
|
||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
|
||||
}
|
||||
if (errorMessage.includes(NPM_COMMAND_TOKENS.NPM_PACKAGE_VERSION_NOT_FOUND_ERROR)) {
|
||||
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_VERSION_NOT_FOUND);
|
||||
}
|
||||
if (
|
||||
errorMessage.includes(NPM_COMMAND_TOKENS.NPM_DISK_NO_SPACE) ||
|
||||
errorMessage.includes(NPM_COMMAND_TOKENS.NPM_DISK_INSUFFICIENT_SPACE)
|
||||
) {
|
||||
throw new Error(RESPONSE_ERROR_MESSAGES.DISK_IS_FULL);
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
LoggerProxy.warn('npm command failed; see message', { errorMessage });
|
||||
const map = {
|
||||
[NPM_PACKAGE_NOT_FOUND_ERROR]: PACKAGE_NOT_FOUND,
|
||||
[NPM_NO_VERSION_AVAILABLE]: PACKAGE_NOT_FOUND,
|
||||
[NPM_PACKAGE_VERSION_NOT_FOUND_ERROR]: PACKAGE_VERSION_NOT_FOUND,
|
||||
[NPM_DISK_NO_SPACE]: DISK_IS_FULL,
|
||||
[NPM_DISK_INSUFFICIENT_SPACE]: DISK_IS_FULL,
|
||||
};
|
||||
|
||||
throw new Error('Package could not be installed - check logs for details');
|
||||
Object.entries(map).forEach(([npmMessage, n8nMessage]) => {
|
||||
if (errorMessage.includes(npmMessage)) throw new Error(n8nMessage);
|
||||
});
|
||||
|
||||
LoggerProxy.warn('npm command failed', { errorMessage });
|
||||
|
||||
throw new Error(PACKAGE_FAILED_TO_INSTALL);
|
||||
}
|
||||
};
|
||||
|
||||
export function matchPackagesWithUpdates(
|
||||
installedPackages: InstalledPackages[],
|
||||
availableUpdates?: NpmUpdatesAvailable,
|
||||
packages: InstalledPackages[],
|
||||
updates?: CommunityPackages.AvailableUpdates,
|
||||
): PublicInstalledPackage[] {
|
||||
if (!availableUpdates) {
|
||||
return installedPackages;
|
||||
}
|
||||
const hydratedPackageList = [] as PublicInstalledPackage[];
|
||||
if (!updates) return packages;
|
||||
|
||||
for (let i = 0; i < installedPackages.length; i++) {
|
||||
const installedPackage = installedPackages[i];
|
||||
const publicPackage = { ...installedPackage } as PublicInstalledPackage;
|
||||
return packages.reduce<PublicInstalledPackage[]>((acc, cur) => {
|
||||
const publicPackage: PublicInstalledPackage = { ...cur };
|
||||
|
||||
if (availableUpdates[installedPackage.packageName]) {
|
||||
publicPackage.updateAvailable = availableUpdates[installedPackage.packageName].latest;
|
||||
}
|
||||
hydratedPackageList.push(publicPackage);
|
||||
}
|
||||
const update = updates[cur.packageName];
|
||||
|
||||
return hydratedPackageList;
|
||||
if (update) publicPackage.updateAvailable = update.latest;
|
||||
|
||||
acc.push(publicPackage);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function matchMissingPackages(
|
||||
|
@ -138,7 +147,7 @@ export function matchMissingPackages(
|
|||
const missingPackagesList = missingPackageNames.map((missingPackageName: string) => {
|
||||
// Strip away versions but maintain scope and package name
|
||||
try {
|
||||
const parsedPackageData = parsePackageName(missingPackageName);
|
||||
const parsedPackageData = parseNpmPackageName(missingPackageName);
|
||||
return parsedPackageData.packageName;
|
||||
|
||||
// eslint-disable-next-line no-empty
|
||||
|
@ -147,6 +156,7 @@ export function matchMissingPackages(
|
|||
});
|
||||
|
||||
const hydratedPackageList = [] as PublicInstalledPackage[];
|
||||
|
||||
installedPackages.forEach((installedPackage) => {
|
||||
const hydratedInstalledPackage = { ...installedPackage };
|
||||
if (missingPackagesList.includes(hydratedInstalledPackage.packageName)) {
|
||||
|
@ -158,47 +168,38 @@ export function matchMissingPackages(
|
|||
return hydratedPackageList;
|
||||
}
|
||||
|
||||
export async function checkPackageStatus(packageName: string): Promise<NpmPackageStatusCheck> {
|
||||
// You can change this URL for testing - the default testing url below
|
||||
// is a postman mock service
|
||||
const n8nBackendServiceUrl = 'https://api.n8n.io/api/package';
|
||||
export async function checkNpmPackageStatus(
|
||||
packageName: string,
|
||||
): Promise<CommunityPackages.PackageStatusCheck> {
|
||||
const N8N_BACKEND_SERVICE_URL = 'https://api.n8n.io/api/package';
|
||||
|
||||
try {
|
||||
const output = await axios.post(
|
||||
n8nBackendServiceUrl,
|
||||
const response = await axios.post<CommunityPackages.PackageStatusCheck>(
|
||||
N8N_BACKEND_SERVICE_URL,
|
||||
{ name: packageName },
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
{ method: 'POST' },
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (output.data.status !== NPM_PACKAGE_STATUS_GOOD) {
|
||||
return output.data as NpmPackageStatusCheck;
|
||||
}
|
||||
if (response.data.status !== NPM_PACKAGE_STATUS_GOOD) return response.data;
|
||||
} catch (error) {
|
||||
// Do nothing if service is unreachable
|
||||
}
|
||||
|
||||
return { status: NPM_PACKAGE_STATUS_GOOD };
|
||||
}
|
||||
|
||||
export function hasPackageLoadedSuccessfully(packageName: string): boolean {
|
||||
try {
|
||||
const failedPackages = (config.get('nodes.packagesMissing') as string).split(' ');
|
||||
export function hasPackageLoaded(packageName: string): boolean {
|
||||
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
||||
|
||||
const packageFailedToLoad = failedPackages.find(
|
||||
if (!missingPackages) return true;
|
||||
|
||||
return !missingPackages
|
||||
.split(' ')
|
||||
.some(
|
||||
(packageNameAndVersion) =>
|
||||
packageNameAndVersion.startsWith(packageName) &&
|
||||
packageNameAndVersion.replace(packageName, '').startsWith('@'),
|
||||
);
|
||||
if (packageFailedToLoad) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (_error) {
|
||||
// If key doesn't exist it means all packages loaded fine
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function removePackageFromMissingList(packageName: string): void {
|
||||
|
@ -216,3 +217,17 @@ export function removePackageFromMissingList(packageName: string): void {
|
|||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export const isClientError = (error: Error): boolean => {
|
||||
const clientErrors = [
|
||||
PACKAGE_VERSION_NOT_FOUND,
|
||||
PACKAGE_DOES_NOT_CONTAIN_NODES,
|
||||
PACKAGE_NOT_FOUND,
|
||||
];
|
||||
|
||||
return clientErrors.some((message) => error.message.includes(message));
|
||||
};
|
||||
|
||||
export function isNpmError(error: unknown): error is { code: number; stdout: string } {
|
||||
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
|
||||
}
|
||||
|
|
|
@ -4,24 +4,25 @@ import { Db } from '..';
|
|||
import { InstalledNodes } from '../databases/entities/InstalledNodes';
|
||||
import { InstalledPackages } from '../databases/entities/InstalledPackages';
|
||||
|
||||
export async function searchInstalledPackage(
|
||||
export async function findInstalledPackage(
|
||||
packageName: string,
|
||||
): Promise<InstalledPackages | undefined> {
|
||||
const installedPackage = await Db.collections.InstalledPackages.findOne(packageName, {
|
||||
relations: ['installedNodes'],
|
||||
});
|
||||
return installedPackage;
|
||||
return Db.collections.InstalledPackages.findOne(packageName, { relations: ['installedNodes'] });
|
||||
}
|
||||
|
||||
export async function isPackageInstalled(packageName: string): Promise<boolean> {
|
||||
const installedPackage = await findInstalledPackage(packageName);
|
||||
return installedPackage !== undefined;
|
||||
}
|
||||
|
||||
export async function getAllInstalledPackages(): Promise<InstalledPackages[]> {
|
||||
const installedPackages = await Db.collections.InstalledPackages.find({
|
||||
relations: ['installedNodes'],
|
||||
});
|
||||
return installedPackages;
|
||||
return Db.collections.InstalledPackages.find({ relations: ['installedNodes'] });
|
||||
}
|
||||
|
||||
export async function removePackageFromDatabase(packageName: InstalledPackages): Promise<void> {
|
||||
void (await Db.collections.InstalledPackages.remove(packageName));
|
||||
export async function removePackageFromDatabase(
|
||||
packageName: InstalledPackages,
|
||||
): Promise<InstalledPackages> {
|
||||
return Db.collections.InstalledPackages.remove(packageName);
|
||||
}
|
||||
|
||||
export async function persistInstalledPackageData(
|
||||
|
|
|
@ -714,30 +714,32 @@ export interface IWorkflowExecuteProcess {
|
|||
|
||||
export type WhereClause = Record<string, { id: string }>;
|
||||
|
||||
/** ********************************
|
||||
* Commuinity nodes
|
||||
******************************** */
|
||||
// ----------------------------------
|
||||
// community nodes
|
||||
// ----------------------------------
|
||||
|
||||
export type ParsedNpmPackageName = {
|
||||
packageName: string;
|
||||
originalString: string;
|
||||
scope?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type NpmUpdatesAvailable = {
|
||||
[packageName: string]: {
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
location: string;
|
||||
export namespace CommunityPackages {
|
||||
export type ParsedPackageName = {
|
||||
packageName: string;
|
||||
rawString: string;
|
||||
scope?: string;
|
||||
version?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type NpmPackageStatusCheck = {
|
||||
status: 'OK' | 'Banned';
|
||||
reason?: string;
|
||||
};
|
||||
export type AvailableUpdates = {
|
||||
[packageName: string]: {
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
location: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type PackageStatusCheck = {
|
||||
status: 'OK' | 'Banned';
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// telemetry
|
||||
|
|
|
@ -35,6 +35,8 @@ export class ResponseError extends Error {
|
|||
|
||||
/**
|
||||
* Creates an instance of ResponseError.
|
||||
* Must be used inside a block with `ResponseHelper.send()`.
|
||||
*
|
||||
* @param {string} message The error message
|
||||
* @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
|
||||
* @param {number} [httpStatusCode] The HTTP status code the response should have
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import express = require('express');
|
||||
import { LoggerProxy, PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { getLogger } from '../Logger';
|
||||
import express from 'express';
|
||||
import { PublicInstalledPackage } from 'n8n-workflow';
|
||||
|
||||
import config from '../../config';
|
||||
import { ResponseHelper, LoadNodesAndCredentials, Push, InternalHooksManager } from '..';
|
||||
import { NodeRequest } from '../requests';
|
||||
import { RESPONSE_ERROR_MESSAGES } from '../constants';
|
||||
|
||||
import { RESPONSE_ERROR_MESSAGES, UNKNOWN_FAILURE_REASON } from '../constants';
|
||||
import {
|
||||
matchMissingPackages,
|
||||
matchPackagesWithUpdates,
|
||||
executeCommand,
|
||||
checkPackageStatus,
|
||||
hasPackageLoadedSuccessfully,
|
||||
checkNpmPackageStatus,
|
||||
hasPackageLoaded,
|
||||
removePackageFromMissingList,
|
||||
parsePackageName,
|
||||
parseNpmPackageName,
|
||||
isClientError,
|
||||
sanitizeNpmPackageName,
|
||||
isNpmError,
|
||||
} from '../CommunityNodes/helpers';
|
||||
import { getAllInstalledPackages, searchInstalledPackage } from '../CommunityNodes/packageModel';
|
||||
import {
|
||||
getAllInstalledPackages,
|
||||
findInstalledPackage,
|
||||
isPackageInstalled,
|
||||
} from '../CommunityNodes/packageModel';
|
||||
import { isAuthenticatedRequest } from '../UserManagement/UserManagementHelper';
|
||||
import config = require('../../config');
|
||||
import { NpmUpdatesAvailable } from '../Interfaces';
|
||||
|
||||
import type { NodeRequest } from '../requests';
|
||||
import type { CommunityPackages } from '../Interfaces';
|
||||
import { InstalledPackages } from '../databases/entities/InstalledPackages';
|
||||
|
||||
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
|
||||
|
||||
export const nodesController = express.Router();
|
||||
|
||||
/**
|
||||
* Initialize Logger if needed
|
||||
*/
|
||||
nodesController.use((req, res, next) => {
|
||||
try {
|
||||
LoggerProxy.getInstance();
|
||||
} catch (error) {
|
||||
LoggerProxy.init(getLogger());
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
nodesController.use((req, res, next) => {
|
||||
if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') {
|
||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -50,246 +50,263 @@ nodesController.use((req, res, next) => {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /nodes
|
||||
*
|
||||
* Install an n8n community package
|
||||
*/
|
||||
nodesController.post(
|
||||
'/',
|
||||
ResponseHelper.send(async (req: NodeRequest.Post) => {
|
||||
const { name } = req.body;
|
||||
let parsedPackageName;
|
||||
try {
|
||||
parsedPackageName = parsePackageName(name);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
||||
throw new ResponseHelper.ResponseError(error.message, undefined, 400);
|
||||
|
||||
if (!name) {
|
||||
throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400);
|
||||
}
|
||||
|
||||
// Only install packages that haven't been installed
|
||||
// or that have failed loading
|
||||
const installedPackageInstalled = await searchInstalledPackage(parsedPackageName.packageName);
|
||||
const loadedPackage = hasPackageLoadedSuccessfully(name);
|
||||
if (installedPackageInstalled && loadedPackage) {
|
||||
let parsed: CommunityPackages.ParsedPackageName;
|
||||
|
||||
try {
|
||||
parsed = parseNpmPackageName(name);
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Package "${parsedPackageName.packageName}" is already installed. For updating, click the corresponding button.`,
|
||||
error instanceof Error ? error.message : 'Failed to parse package name',
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const packageStatus = await checkPackageStatus(name);
|
||||
const isInstalled = await isPackageInstalled(parsed.packageName);
|
||||
const hasLoaded = hasPackageLoaded(name);
|
||||
|
||||
if (isInstalled && hasLoaded) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
[
|
||||
`Package "${parsed.packageName}" is already installed`,
|
||||
'To update it, click the corresponding button in the UI',
|
||||
].join('.'),
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const packageStatus = await checkNpmPackageStatus(name);
|
||||
|
||||
if (packageStatus.status !== 'OK') {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Package "${name}" has been banned from n8n's repository and will not be installed`,
|
||||
`Package "${name}" is banned so it cannot be installed`,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
let installedPackage: InstalledPackages;
|
||||
|
||||
try {
|
||||
const installedPackage = await LoadNodesAndCredentials().loadNpmModule(
|
||||
parsedPackageName.packageName,
|
||||
parsedPackageName.version,
|
||||
installedPackage = await LoadNodesAndCredentials().loadNpmModule(
|
||||
parsed.packageName,
|
||||
parsed.version,
|
||||
);
|
||||
|
||||
if (!loadedPackage) {
|
||||
removePackageFromMissingList(name);
|
||||
}
|
||||
|
||||
// Inform the connected frontends that new nodes are available
|
||||
installedPackage.installedNodes.forEach((nodeData) => {
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('reloadNodeType', {
|
||||
name: nodeData.type,
|
||||
version: nodeData.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({
|
||||
user_id: req.user.id,
|
||||
input_string: name,
|
||||
package_name: parsedPackageName.packageName,
|
||||
success: true,
|
||||
package_version: parsedPackageName.version,
|
||||
package_node_names: installedPackage.installedNodes.map((nodeData) => nodeData.name),
|
||||
package_author: installedPackage.authorName,
|
||||
package_author_email: installedPackage.authorEmail,
|
||||
});
|
||||
|
||||
return installedPackage;
|
||||
} catch (error) {
|
||||
let statusCode = 500;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const errorMessage = error.message as string;
|
||||
if (
|
||||
errorMessage.includes(RESPONSE_ERROR_MESSAGES.PACKAGE_VERSION_NOT_FOUND) ||
|
||||
errorMessage.includes(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES) ||
|
||||
errorMessage.includes(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND)
|
||||
) {
|
||||
statusCode = 400;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({
|
||||
user_id: req.user.id,
|
||||
input_string: name,
|
||||
package_name: parsedPackageName.packageName,
|
||||
package_name: parsed.packageName,
|
||||
success: false,
|
||||
package_version: parsedPackageName.version,
|
||||
package_version: parsed.version,
|
||||
failure_reason: errorMessage,
|
||||
});
|
||||
throw new ResponseHelper.ResponseError(
|
||||
`Error loading package "${name}": ${errorMessage}`,
|
||||
undefined,
|
||||
statusCode,
|
||||
);
|
||||
|
||||
const message = [`Error loading package "${name}"`, errorMessage].join(':');
|
||||
|
||||
const clientError = error instanceof Error ? isClientError(error) : false;
|
||||
|
||||
throw new ResponseHelper.ResponseError(message, undefined, clientError ? 400 : 500);
|
||||
}
|
||||
|
||||
if (!hasLoaded) removePackageFromMissingList(name);
|
||||
|
||||
const pushInstance = Push.getInstance();
|
||||
|
||||
// broadcast to connected frontends that node list has been updated
|
||||
installedPackage.installedNodes.forEach((node) => {
|
||||
pushInstance.send('reloadNodeType', {
|
||||
name: node.type,
|
||||
version: node.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({
|
||||
user_id: req.user.id,
|
||||
input_string: name,
|
||||
package_name: parsed.packageName,
|
||||
success: true,
|
||||
package_version: parsed.version,
|
||||
package_node_names: installedPackage.installedNodes.map((node) => node.name),
|
||||
package_author: installedPackage.authorName,
|
||||
package_author_email: installedPackage.authorEmail,
|
||||
});
|
||||
|
||||
return installedPackage;
|
||||
}),
|
||||
);
|
||||
|
||||
// Install new credentials/nodes from npm
|
||||
/**
|
||||
* GET /nodes
|
||||
*
|
||||
* Retrieve list of installed n8n community packages
|
||||
*/
|
||||
nodesController.get(
|
||||
'/',
|
||||
ResponseHelper.send(async (): Promise<PublicInstalledPackage[]> => {
|
||||
const packages = await getAllInstalledPackages();
|
||||
const installedPackages = await getAllInstalledPackages();
|
||||
|
||||
if (packages.length === 0) {
|
||||
return packages;
|
||||
}
|
||||
if (installedPackages.length === 0) return [];
|
||||
|
||||
let pendingUpdates: CommunityPackages.AvailableUpdates | undefined;
|
||||
|
||||
let pendingUpdates: NpmUpdatesAvailable | undefined;
|
||||
try {
|
||||
// Command succeeds when there are no updates.
|
||||
// NPM handles this oddly. It exits with code 1 when there are updates.
|
||||
// More here: https://github.com/npm/rfcs/issues/473
|
||||
await executeCommand('npm outdated --json', { doNotHandleError: true });
|
||||
const command = ['npm', 'outdated', '--json'].join(' ');
|
||||
await executeCommand(command, { doNotHandleError: true });
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
if (error.code === 1) {
|
||||
// Updates available
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
|
||||
pendingUpdates = JSON.parse(error.stdout);
|
||||
// when there are updates, npm exits with code 1
|
||||
// when there are no updates, command succeeds
|
||||
// https://github.com/npm/rfcs/issues/473
|
||||
|
||||
if (isNpmError(error) && error.code === 1) {
|
||||
pendingUpdates = JSON.parse(error.stdout) as CommunityPackages.AvailableUpdates;
|
||||
}
|
||||
}
|
||||
let hydratedPackages = matchPackagesWithUpdates(packages, pendingUpdates);
|
||||
|
||||
let hydratedPackages = matchPackagesWithUpdates(installedPackages, pendingUpdates);
|
||||
|
||||
try {
|
||||
if (config.get('nodes.packagesMissing')) {
|
||||
// eslint-disable-next-line prettier/prettier, @typescript-eslint/no-unsafe-argument
|
||||
hydratedPackages = matchMissingPackages(hydratedPackages, config.get('nodes.packagesMissing'));
|
||||
const missingPackages = config.get('nodes.packagesMissing') as string | undefined;
|
||||
|
||||
if (missingPackages) {
|
||||
hydratedPackages = matchMissingPackages(hydratedPackages, missingPackages);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
// Do nothing if setting is missing
|
||||
}
|
||||
|
||||
return hydratedPackages;
|
||||
}),
|
||||
);
|
||||
|
||||
// Uninstall credentials/nodes from npm
|
||||
/**
|
||||
* DELETE /nodes
|
||||
*
|
||||
* Uninstall an installed n8n community package
|
||||
*/
|
||||
nodesController.delete(
|
||||
'/',
|
||||
ResponseHelper.send(async (req: NodeRequest.Delete) => {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NAME_NOT_PROVIDED,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
}
|
||||
// This function also sanitizes the package name by throwing errors.
|
||||
parsePackageName(name);
|
||||
|
||||
const installedPackage = await searchInstalledPackage(name);
|
||||
|
||||
if (!installedPackage) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_INSTALLED,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
void (await LoadNodesAndCredentials().removeNpmModule(name, installedPackage));
|
||||
|
||||
// Inform the connected frontends that the node list has been updated
|
||||
installedPackage.installedNodes.forEach((installedNode) => {
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('removeNodeType', {
|
||||
name: installedNode.type,
|
||||
version: installedNode.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageDeleteFinished({
|
||||
user_id: req.user.id,
|
||||
package_name: name,
|
||||
package_version: installedPackage.installedVersion,
|
||||
package_node_names: installedPackage.installedNodes.map((nodeData) => nodeData.name),
|
||||
package_author: installedPackage.authorName,
|
||||
package_author_email: installedPackage.authorEmail,
|
||||
});
|
||||
sanitizeNpmPackageName(name);
|
||||
} catch (error) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||
`Error removing package "${name}": ${error.message}`,
|
||||
undefined,
|
||||
500,
|
||||
);
|
||||
const message = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
|
||||
|
||||
throw new ResponseHelper.ResponseError(message, undefined, 400);
|
||||
}
|
||||
|
||||
const installedPackage = await findInstalledPackage(name);
|
||||
|
||||
if (!installedPackage) {
|
||||
throw new ResponseHelper.ResponseError(PACKAGE_NOT_INSTALLED, undefined, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
await LoadNodesAndCredentials().removeNpmModule(name, installedPackage);
|
||||
} catch (error) {
|
||||
const message = [
|
||||
`Error removing package "${name}"`,
|
||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||
].join(':');
|
||||
|
||||
throw new ResponseHelper.ResponseError(message, undefined, 500);
|
||||
}
|
||||
|
||||
const pushInstance = Push.getInstance();
|
||||
|
||||
// broadcast to connected frontends that node list has been updated
|
||||
installedPackage.installedNodes.forEach((node) => {
|
||||
pushInstance.send('removeNodeType', {
|
||||
name: node.type,
|
||||
version: node.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageDeleteFinished({
|
||||
user_id: req.user.id,
|
||||
package_name: name,
|
||||
package_version: installedPackage.installedVersion,
|
||||
package_node_names: installedPackage.installedNodes.map((node) => node.name),
|
||||
package_author: installedPackage.authorName,
|
||||
package_author_email: installedPackage.authorEmail,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Update a package
|
||||
/**
|
||||
* PATCH /nodes
|
||||
*
|
||||
* Update an installed n8n community package
|
||||
*/
|
||||
nodesController.patch(
|
||||
'/',
|
||||
ResponseHelper.send(async (req: NodeRequest.Update) => {
|
||||
const { name } = req.body;
|
||||
|
||||
if (!name) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NAME_NOT_PROVIDED,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
throw new ResponseHelper.ResponseError(PACKAGE_NAME_NOT_PROVIDED, undefined, 400);
|
||||
}
|
||||
|
||||
const parsedPackageData = parsePackageName(name);
|
||||
const packagePreviouslyInstalled = await searchInstalledPackage(name);
|
||||
const previouslyInstalledPackage = await findInstalledPackage(name);
|
||||
|
||||
if (!packagePreviouslyInstalled) {
|
||||
throw new ResponseHelper.ResponseError(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_INSTALLED,
|
||||
undefined,
|
||||
400,
|
||||
);
|
||||
if (!previouslyInstalledPackage) {
|
||||
throw new ResponseHelper.ResponseError(PACKAGE_NOT_INSTALLED, undefined, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const newInstalledPackage = await LoadNodesAndCredentials().updateNpmModule(
|
||||
parsedPackageData.packageName,
|
||||
packagePreviouslyInstalled,
|
||||
parseNpmPackageName(name).packageName,
|
||||
previouslyInstalledPackage,
|
||||
);
|
||||
|
||||
const pushInstance = Push.getInstance();
|
||||
|
||||
// Inform the connected frontends that new nodes are available
|
||||
packagePreviouslyInstalled.installedNodes.forEach((installedNode) => {
|
||||
// broadcast to connected frontends that node list has been updated
|
||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||
pushInstance.send('removeNodeType', {
|
||||
name: installedNode.type,
|
||||
version: installedNode.latestVersion,
|
||||
name: node.type,
|
||||
version: node.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
newInstalledPackage.installedNodes.forEach((nodeData) => {
|
||||
newInstalledPackage.installedNodes.forEach((node) => {
|
||||
pushInstance.send('reloadNodeType', {
|
||||
name: nodeData.name,
|
||||
version: nodeData.latestVersion,
|
||||
name: node.name,
|
||||
version: node.latestVersion,
|
||||
});
|
||||
});
|
||||
|
||||
void InternalHooksManager.getInstance().onCommunityPackageUpdateFinished({
|
||||
user_id: req.user.id,
|
||||
package_name: name,
|
||||
package_version_current: packagePreviouslyInstalled.installedVersion,
|
||||
package_version_current: previouslyInstalledPackage.installedVersion,
|
||||
package_version_new: newInstalledPackage.installedVersion,
|
||||
package_node_names: newInstalledPackage.installedNodes.map((node) => node.name),
|
||||
package_author: newInstalledPackage.authorName,
|
||||
|
@ -298,19 +315,20 @@ nodesController.patch(
|
|||
|
||||
return newInstalledPackage;
|
||||
} catch (error) {
|
||||
packagePreviouslyInstalled.installedNodes.forEach((installedNode) => {
|
||||
previouslyInstalledPackage.installedNodes.forEach((node) => {
|
||||
const pushInstance = Push.getInstance();
|
||||
pushInstance.send('removeNodeType', {
|
||||
name: installedNode.type,
|
||||
version: installedNode.latestVersion,
|
||||
name: node.type,
|
||||
version: node.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}`,
|
||||
undefined,
|
||||
500,
|
||||
);
|
||||
|
||||
const message = [
|
||||
`Error removing package "${name}"`,
|
||||
error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON,
|
||||
].join(':');
|
||||
|
||||
throw new ResponseHelper.ResponseError(message, undefined, 500);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ export const RESPONSE_ERROR_MESSAGES = {
|
|||
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_FAILED_TO_INSTALL: 'Package could not be installed - check logs for details',
|
||||
PACKAGE_NOT_FOUND: 'Package not found in npm',
|
||||
PACKAGE_VERSION_NOT_FOUND: 'The specified package version was not found',
|
||||
PACKAGE_DOES_NOT_CONTAIN_NODES: 'The specified package does not contain any nodes',
|
||||
|
@ -29,3 +30,5 @@ export const NPM_COMMAND_TOKENS = {
|
|||
};
|
||||
|
||||
export const NPM_PACKAGE_STATUS_GOOD = 'OK';
|
||||
|
||||
export const UNKNOWN_FAILURE_REASON = 'Unknown failure reason';
|
||||
|
|
12
packages/cli/src/requests.d.ts
vendored
12
packages/cli/src/requests.d.ts
vendored
|
@ -286,21 +286,21 @@ export type NodeParameterOptionsRequest = AuthenticatedRequest<
|
|||
>;
|
||||
|
||||
// ----------------------------------
|
||||
// /tags
|
||||
// /tags
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace TagsRequest {
|
||||
type Delete = AuthenticatedRequest<{ id: string }>;
|
||||
}
|
||||
|
||||
export declare namespace NodeRequest {
|
||||
type RequestBody = {
|
||||
name: string;
|
||||
};
|
||||
// ----------------------------------
|
||||
// /nodes
|
||||
// ----------------------------------
|
||||
|
||||
export declare namespace NodeRequest {
|
||||
type GetAll = AuthenticatedRequest;
|
||||
|
||||
type Post = AuthenticatedRequest<{}, {}, RequestBody>;
|
||||
type Post = AuthenticatedRequest<{}, {}, { name?: string }>;
|
||||
|
||||
type Delete = Post;
|
||||
|
||||
|
|
|
@ -1,341 +1,333 @@
|
|||
import { exec } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
import express from 'express';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import * as utils from './shared/utils';
|
||||
import type { InstalledNodePayload, InstalledPackagePayload } from './shared/types';
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import type { User } from '../../src/databases/entities/User';
|
||||
import * as testDb from './shared/testDb';
|
||||
|
||||
jest.mock('../../src/CommunityNodes/helpers', () => ({
|
||||
matchPackagesWithUpdates: jest.requireActual('../../src/CommunityNodes/helpers').matchPackagesWithUpdates,
|
||||
parsePackageName: jest.requireActual('../../src/CommunityNodes/helpers').parsePackageName,
|
||||
hasPackageLoadedSuccessfully: jest.fn(),
|
||||
searchInstalledPackage: jest.fn(),
|
||||
executeCommand: jest.fn(),
|
||||
checkPackageStatus: jest.fn(),
|
||||
removePackageFromMissingList: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../src/CommunityNodes/packageModel', () => ({
|
||||
getAllInstalledPackages: jest.requireActual('../../src/CommunityNodes/packageModel').getAllInstalledPackages,
|
||||
removePackageFromDatabase: jest.fn(),
|
||||
searchInstalledPackage: jest.fn(),
|
||||
}));
|
||||
|
||||
import { executeCommand, checkPackageStatus, hasPackageLoadedSuccessfully, removePackageFromMissingList } from '../../src/CommunityNodes/helpers';
|
||||
import { getAllInstalledPackages, searchInstalledPackage, removePackageFromDatabase } from '../../src/CommunityNodes/packageModel';
|
||||
import {
|
||||
executeCommand,
|
||||
checkNpmPackageStatus,
|
||||
hasPackageLoaded,
|
||||
removePackageFromMissingList,
|
||||
isNpmError,
|
||||
} from '../../src/CommunityNodes/helpers';
|
||||
import { findInstalledPackage, isPackageInstalled } from '../../src/CommunityNodes/packageModel';
|
||||
import { CURRENT_PACKAGE_VERSION, UPDATED_PACKAGE_VERSION } from './shared/constants';
|
||||
import { installedPackagePayload } from './shared/utils';
|
||||
import { LoadNodesAndCredentials } from '../../src/LoadNodesAndCredentials';
|
||||
import { InstalledPackages } from '../../src/databases/entities/InstalledPackages';
|
||||
|
||||
import type { Role } from '../../src/databases/entities/Role';
|
||||
import type { AuthAgent } from './shared/types';
|
||||
import type { InstalledNodes } from '../../src/databases/entities/InstalledNodes';
|
||||
|
||||
jest.mock('../../src/telemetry');
|
||||
|
||||
jest.mock('../../src/LoadNodesAndCredentials', () => ({
|
||||
LoadNodesAndCredentials: jest.fn(),
|
||||
}));
|
||||
import { LoadNodesAndCredentials } from '../../src/LoadNodesAndCredentials';
|
||||
jest.mock('../../src/CommunityNodes/helpers', () => {
|
||||
return {
|
||||
...jest.requireActual('../../src/CommunityNodes/helpers'),
|
||||
checkNpmPackageStatus: jest.fn(),
|
||||
executeCommand: jest.fn(),
|
||||
hasPackageLoaded: jest.fn(),
|
||||
isNpmError: jest.fn(),
|
||||
removePackageFromMissingList: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../src/CommunityNodes/packageModel', () => {
|
||||
return {
|
||||
...jest.requireActual('../../src/CommunityNodes/packageModel'),
|
||||
isPackageInstalled: jest.fn(),
|
||||
findInstalledPackage: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockedEmptyPackage = mocked(utils.emptyPackage);
|
||||
|
||||
let app: express.Application;
|
||||
let testDbName = '';
|
||||
let globalOwnerRole: Role;
|
||||
let globalMemberRole: Role;
|
||||
let ownerShell: User;
|
||||
let authAgent: AuthAgent;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await utils.initTestServer({ endpointGroups: ['nodes'], applyAuth: true });
|
||||
const initResult = await testDb.init();
|
||||
testDbName = initResult.testDbName;
|
||||
|
||||
utils.initConfigFile();
|
||||
|
||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||
globalMemberRole = await testDb.getGlobalMemberRole();
|
||||
ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
authAgent = utils.createAuthAgent(app);
|
||||
|
||||
utils.initConfigFile();
|
||||
utils.initTestLogger();
|
||||
utils.initTestTelemetry();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testDb.truncate(['InstalledNodes', 'InstalledPackages'], testDbName);
|
||||
// @ts-ignore
|
||||
executeCommand.mockReset();
|
||||
// @ts-ignore
|
||||
checkPackageStatus.mockReset();
|
||||
// @ts-ignore
|
||||
searchInstalledPackage.mockReset();
|
||||
// @ts-ignore
|
||||
hasPackageLoadedSuccessfully.mockReset();
|
||||
|
||||
mocked(executeCommand).mockReset();
|
||||
mocked(findInstalledPackage).mockReset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate(testDbName);
|
||||
});
|
||||
|
||||
test('GET /nodes should return empty list when no nodes are installed', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
/**
|
||||
* GET /nodes
|
||||
*/
|
||||
|
||||
const response = await authOwnerAgent.get('/nodes').send();
|
||||
test('GET /nodes should respond 200 if no nodes are installed', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data).toHaveLength(0);
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('GET /nodes should return list with installed package and node', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const installedPackage = await saveMockPackage(installedPackagePayload());
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage.packageName));
|
||||
test('GET /nodes should return list of one installed package and node', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const response = await authOwnerAgent.get('/nodes').send();
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data).toHaveLength(1);
|
||||
expect(response.body.data[0].installedNodes).toHaveLength(1);
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(1);
|
||||
expect(data[0].installedNodes).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('GET /nodes should return list with multiple installed package and node', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const installedPackage1 = await saveMockPackage(installedPackagePayload());
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage1.packageName));
|
||||
test('GET /nodes should return list of multiple installed packages and nodes', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const installedPackage2 = await saveMockPackage(installedPackagePayload());
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage2.packageName));
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage2.packageName));
|
||||
const first = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(first.packageName));
|
||||
|
||||
const response = await authOwnerAgent.get('/nodes').send();
|
||||
const second = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(second.packageName));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body.data).toHaveLength(2);
|
||||
expect([...response.body.data[0].installedNodes, ...response.body.data[1].installedNodes]).toHaveLength(3);
|
||||
const {
|
||||
statusCode,
|
||||
body: { data },
|
||||
} = await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(data).toHaveLength(2);
|
||||
|
||||
const allNodes = data.reduce(
|
||||
(acc: InstalledNodes[], cur: InstalledPackages) => acc.concat(cur.installedNodes),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(allNodes).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('GET /nodes should not check for updates when there are no packages installed', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
test('GET /nodes should not check for updates if no packages installed', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
await authOwnerAgent.get('/nodes').send();
|
||||
await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledTimes(0);
|
||||
expect(mocked(executeCommand)).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('GET /nodes should check for updates when there are packages installed', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const installedPackage = await saveMockPackage(installedPackagePayload());
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage.packageName));
|
||||
test('GET /nodes should check for updates if packages installed', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
await authOwnerAgent.get('/nodes').send();
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
|
||||
expect(executeCommand).toHaveBeenCalledWith('npm outdated --json', {"doNotHandleError": true});
|
||||
await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(mocked(executeCommand)).toHaveBeenCalledWith('npm outdated --json', {
|
||||
doNotHandleError: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('GET /nodes should mention updates when available', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const installedPackage = await saveMockPackage(installedPackagePayload());
|
||||
await saveMockNode(utils.installedNodePayload(installedPackage.packageName));
|
||||
test('GET /nodes should report package updates if available', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
// @ts-ignore
|
||||
executeCommand.mockImplementation(() => {
|
||||
throw getNpmOutdatedError(installedPackage.packageName);
|
||||
const { packageName } = await testDb.saveInstalledPackage(utils.installedPackagePayload());
|
||||
await testDb.saveInstalledNode(utils.installedNodePayload(packageName));
|
||||
|
||||
mocked(executeCommand).mockImplementationOnce(() => {
|
||||
throw {
|
||||
code: 1,
|
||||
stdout: JSON.stringify({
|
||||
[packageName]: {
|
||||
current: CURRENT_PACKAGE_VERSION,
|
||||
wanted: CURRENT_PACKAGE_VERSION,
|
||||
latest: UPDATED_PACKAGE_VERSION,
|
||||
location: path.join('node_modules', packageName),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const response = await authOwnerAgent.get('/nodes').send();
|
||||
expect(response.body.data[0].installedVersion).toBe(CURRENT_PACKAGE_VERSION);
|
||||
expect(response.body.data[0].updateAvailable).toBe(UPDATED_PACKAGE_VERSION);
|
||||
mocked(isNpmError).mockReturnValueOnce(true);
|
||||
|
||||
const {
|
||||
body: { data },
|
||||
} = await authAgent(ownerShell).get('/nodes');
|
||||
|
||||
expect(data[0].installedVersion).toBe(CURRENT_PACKAGE_VERSION);
|
||||
expect(data[0].updateAvailable).toBe(UPDATED_PACKAGE_VERSION);
|
||||
});
|
||||
|
||||
// TEST POST ENDPOINT
|
||||
/**
|
||||
* POST /nodes
|
||||
*/
|
||||
|
||||
test('POST /nodes package name should not be empty', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const response = await authOwnerAgent.post('/nodes').send();
|
||||
test('POST /nodes should reject if package name is missing', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const { statusCode } = await authAgent(ownerShell).post('/nodes');
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST /nodes should reject if package is duplicate', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
|
||||
mocked(isPackageInstalled).mockResolvedValueOnce(true);
|
||||
mocked(hasPackageLoaded).mockReturnValueOnce(true);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authAgent(ownerShell).post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('already installed');
|
||||
});
|
||||
|
||||
test('POST /nodes should allow installing packages that could not be loaded', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
mocked(findInstalledPackage).mockResolvedValueOnce(new InstalledPackages());
|
||||
mocked(hasPackageLoaded).mockReturnValueOnce(false);
|
||||
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
|
||||
|
||||
jest
|
||||
.spyOn(LoadNodesAndCredentials(), 'loadNpmModule')
|
||||
.mockImplementationOnce(mockedEmptyPackage);
|
||||
|
||||
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(200);
|
||||
expect(mocked(removePackageFromMissingList)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('POST /nodes should not install a banned package', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'Banned' });
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authAgent(ownerShell).post('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('banned');
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /nodes
|
||||
*/
|
||||
|
||||
test('DELETE /nodes should not delete if package name is empty', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const response = await authAgent(ownerShell).delete('/nodes');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('POST /nodes Should not install duplicate packages', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
// @ts-ignore
|
||||
searchInstalledPackage.mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
// @ts-ignore
|
||||
hasPackageLoadedSuccessfully.mockImplementation(() => {
|
||||
return true;
|
||||
test('DELETE /nodes should reject if package is not installed', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authAgent(ownerShell).delete('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
const response = await authOwnerAgent.post('/nodes').send(requestBody);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('already installed');
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('not installed');
|
||||
});
|
||||
|
||||
test('POST /nodes Should allow installing packages that could not be loaded', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
// @ts-ignore
|
||||
searchInstalledPackage.mockImplementation(() => {
|
||||
return true;
|
||||
});
|
||||
// @ts-ignore
|
||||
hasPackageLoadedSuccessfully.mockImplementation(() => {
|
||||
return false;
|
||||
test('DELETE /nodes should uninstall package', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const removeSpy = jest
|
||||
.spyOn(LoadNodesAndCredentials(), 'removeNpmModule')
|
||||
.mockImplementationOnce(jest.fn());
|
||||
|
||||
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
|
||||
|
||||
const { statusCode } = await authAgent(ownerShell).delete('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
checkPackageStatus.mockImplementation(() => {
|
||||
return {status:'OK'};
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
LoadNodesAndCredentials.mockImplementation(() => {
|
||||
return {
|
||||
loadNpmModule: () => {
|
||||
return {
|
||||
installedNodes: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const response = await authOwnerAgent.post('/nodes').send(requestBody);
|
||||
|
||||
expect(removePackageFromMissingList).toHaveBeenCalled();
|
||||
expect(response.status).toBe(200);
|
||||
expect(statusCode).toBe(200);
|
||||
expect(removeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('POST /nodes package should not install banned package', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const installedPackage = installedPackagePayload();
|
||||
const requestBody = {
|
||||
name: installedPackage.packageName,
|
||||
};
|
||||
/**
|
||||
* PATCH /nodes
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
checkPackageStatus.mockImplementation(() => {
|
||||
return {status:'Banned'};
|
||||
});
|
||||
const response = await authOwnerAgent.post('/nodes').send(requestBody);
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body.message).toContain('banned');
|
||||
});
|
||||
test('PATCH /nodes should reject if package name is empty', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
// TEST DELETE ENDPOINT
|
||||
test('DELETE /nodes package name should not be empty', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const response = await authOwnerAgent.delete('/nodes').send();
|
||||
const response = await authAgent(ownerShell).patch('/nodes');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('DELETE /nodes Should return error when package was not installed', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
test('PATCH /nodes reject if package is not installed', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const response = await authOwnerAgent.delete('/nodes').send(requestBody);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('not installed');
|
||||
});
|
||||
|
||||
// Useful test ?
|
||||
test('DELETE /nodes package should be uninstall all conditions are true', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
// @ts-ignore
|
||||
searchInstalledPackage.mockImplementation(() => {
|
||||
return {
|
||||
installedNodes: [],
|
||||
};
|
||||
const {
|
||||
statusCode,
|
||||
body: { message },
|
||||
} = await authAgent(ownerShell).patch('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
const removeNpmModuleMock = jest.fn();
|
||||
// @ts-ignore
|
||||
LoadNodesAndCredentials.mockImplementation(() => {
|
||||
return {
|
||||
removeNpmModule: removeNpmModuleMock,
|
||||
};
|
||||
expect(statusCode).toBe(400);
|
||||
expect(message).toContain('not installed');
|
||||
});
|
||||
|
||||
test('PATCH /nodes should update a package', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
|
||||
const updateSpy = jest
|
||||
.spyOn(LoadNodesAndCredentials(), 'updateNpmModule')
|
||||
.mockImplementationOnce(mockedEmptyPackage);
|
||||
|
||||
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
|
||||
|
||||
await authAgent(ownerShell).patch('/nodes').send({
|
||||
name: utils.installedPackagePayload().packageName,
|
||||
});
|
||||
|
||||
const response = await authOwnerAgent.delete('/nodes').send(requestBody);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(removeNpmModuleMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// TEST PATCH ENDPOINT
|
||||
|
||||
test('PATCH /nodes package name should not be empty', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const response = await authOwnerAgent.patch('/nodes').send();
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test('PATCH /nodes Should return error when package was not installed', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
|
||||
const response = await authOwnerAgent.patch('/nodes').send(requestBody);
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toContain('not installed');
|
||||
});
|
||||
|
||||
test('PATCH /nodes package should be updated if all conditions are true', async () => {
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
const requestBody = {
|
||||
name: installedPackagePayload().packageName,
|
||||
};
|
||||
// @ts-ignore
|
||||
searchInstalledPackage.mockImplementation(() => {
|
||||
return {
|
||||
installedNodes: [],
|
||||
};
|
||||
});
|
||||
|
||||
const updatedNpmModuleMock = jest.fn(() => ({
|
||||
installedNodes: [],
|
||||
}));
|
||||
|
||||
// @ts-ignore
|
||||
LoadNodesAndCredentials.mockImplementation(() => {
|
||||
return {
|
||||
updateNpmModule: updatedNpmModuleMock,
|
||||
};
|
||||
});
|
||||
|
||||
const response = await authOwnerAgent.patch('/nodes').send(requestBody);
|
||||
expect(updatedNpmModuleMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
async function saveMockPackage(payload: InstalledPackagePayload) {
|
||||
return await testDb.saveInstalledPackage(payload);
|
||||
}
|
||||
|
||||
async function saveMockNode(payload: InstalledNodePayload) {
|
||||
return await testDb.saveInstalledNode(payload);
|
||||
}
|
||||
|
||||
function getNpmOutdatedError(packageName: string) {
|
||||
const errorOutput = new Error('Something went wrong');
|
||||
// @ts-ignore
|
||||
errorOutput.code = 1;
|
||||
// @ts-ignore
|
||||
errorOutput.stdout = '{' +
|
||||
`"${packageName}": {` +
|
||||
`"current": "${CURRENT_PACKAGE_VERSION}",` +
|
||||
`"wanted": "${CURRENT_PACKAGE_VERSION}",` +
|
||||
`"latest": "${UPDATED_PACKAGE_VERSION}",` +
|
||||
`"location": "node_modules/${packageName}"` +
|
||||
'}' +
|
||||
'}';
|
||||
|
||||
return errorOutput;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type { ICredentialDataDecryptedObject, ICredentialNodeAccess } from 'n8n-workflow';
|
||||
import type { SuperAgentTest } from 'supertest';
|
||||
|
||||
import type { ICredentialsDb, IDatabaseCollections } from '../../../src';
|
||||
import type { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
|
||||
import type { User } from '../../../src/databases/entities/User';
|
||||
|
@ -10,6 +12,8 @@ export type MappingName = keyof typeof MAPPING_TABLES;
|
|||
|
||||
export type ApiPath = 'internal' | 'public';
|
||||
|
||||
export type AuthAgent = (user: User) => SuperAgentTest;
|
||||
|
||||
type EndpointGroup =
|
||||
| 'me'
|
||||
| 'users'
|
||||
|
@ -49,12 +53,11 @@ export interface TriggerTime {
|
|||
export type InstalledPackagePayload = {
|
||||
packageName: string;
|
||||
installedVersion: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type InstalledNodePayload = {
|
||||
name: string;
|
||||
type: string;
|
||||
latestVersion: string;
|
||||
package: string;
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -23,7 +23,12 @@ import {
|
|||
} from 'n8n-workflow';
|
||||
|
||||
import config from '../../../config';
|
||||
import { AUTHLESS_ENDPOINTS, CURRENT_PACKAGE_VERSION, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } from './constants';
|
||||
import {
|
||||
AUTHLESS_ENDPOINTS,
|
||||
CURRENT_PACKAGE_VERSION,
|
||||
PUBLIC_API_REST_PATH_SEGMENT,
|
||||
REST_PATH_SEGMENT,
|
||||
} from './constants';
|
||||
import { AUTH_COOKIE_NAME, NODE_PACKAGE_PREFIX } from '../../../src/constants';
|
||||
import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes';
|
||||
import {
|
||||
|
@ -56,6 +61,7 @@ import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
|||
import { workflowsController } from '../../../src/api/workflows.api';
|
||||
import { nodesController } from '../../../src/api/nodes.api';
|
||||
import { randomName } from './random';
|
||||
import { InstalledPackages } from '../../../src/databases/entities/InstalledPackages';
|
||||
|
||||
/**
|
||||
* Initialize a test server.
|
||||
|
@ -101,7 +107,7 @@ export async function initTestServer({
|
|||
credentials: { controller: credentialsController, path: 'credentials' },
|
||||
workflows: { controller: workflowsController, path: 'workflows' },
|
||||
nodes: { controller: nodesController, path: 'nodes' },
|
||||
publicApi: apiRouters
|
||||
publicApi: apiRouters,
|
||||
};
|
||||
|
||||
for (const group of routerEndpoints) {
|
||||
|
@ -139,6 +145,9 @@ export function initTestTelemetry() {
|
|||
void InternalHooksManager.init('test-instance-id', 'test-version', mockNodeTypes);
|
||||
}
|
||||
|
||||
export const createAuthAgent = (app: express.Application) => (user: User) =>
|
||||
createAgent(app, { auth: true, user });
|
||||
|
||||
/**
|
||||
* Classify endpoint groups into `routerEndpoints` (newest, using `express.Router`),
|
||||
* and `functionEndpoints` (legacy, namespaced inside a function).
|
||||
|
@ -893,7 +902,7 @@ export function getPostgresSchemaSection(
|
|||
}
|
||||
|
||||
// ----------------------------------
|
||||
// nodes
|
||||
// community nodes
|
||||
// ----------------------------------
|
||||
|
||||
export function installedPackagePayload(): InstalledPackagePayload {
|
||||
|
@ -912,3 +921,11 @@ export function installedNodePayload(packageName: string): InstalledNodePayload
|
|||
package: packageName,
|
||||
};
|
||||
}
|
||||
|
||||
export const emptyPackage = () => {
|
||||
const installedPackage = new InstalledPackages();
|
||||
installedPackage.installedNodes = [];
|
||||
|
||||
return Promise.resolve(installedPackage);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,330 +1,343 @@
|
|||
import { checkPackageStatus, matchPackagesWithUpdates, executeCommand, parsePackageName, matchMissingPackages, hasPackageLoadedSuccessfully, removePackageFromMissingList } from '../../src/CommunityNodes/helpers';
|
||||
import { NODE_PACKAGE_PREFIX, NPM_COMMAND_TOKENS, NPM_PACKAGE_STATUS_GOOD, RESPONSE_ERROR_MESSAGES } from '../../src/constants';
|
||||
|
||||
jest.mock('fs/promises');
|
||||
import { exec } from 'child_process';
|
||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||
|
||||
jest.mock('child_process');
|
||||
import { exec } from 'child_process';
|
||||
import { InstalledPackages } from '../../src/databases/entities/InstalledPackages';
|
||||
import { installedNodePayload, installedPackagePayload } from '../integration/shared/utils';
|
||||
import { InstalledNodes } from '../../src/databases/entities/InstalledNodes';
|
||||
import { NpmUpdatesAvailable } from '../../src/Interfaces';
|
||||
import { randomName } from '../integration/shared/random';
|
||||
|
||||
import config from '../../config';
|
||||
|
||||
jest.mock('axios');
|
||||
import axios from 'axios';
|
||||
|
||||
describe('CommunityNodesHelper', () => {
|
||||
import {
|
||||
checkNpmPackageStatus,
|
||||
matchPackagesWithUpdates,
|
||||
executeCommand,
|
||||
parseNpmPackageName,
|
||||
matchMissingPackages,
|
||||
hasPackageLoaded,
|
||||
removePackageFromMissingList,
|
||||
} from '../../src/CommunityNodes/helpers';
|
||||
import {
|
||||
NODE_PACKAGE_PREFIX,
|
||||
NPM_COMMAND_TOKENS,
|
||||
NPM_PACKAGE_STATUS_GOOD,
|
||||
RESPONSE_ERROR_MESSAGES,
|
||||
} from '../../src/constants';
|
||||
import { InstalledPackages } from '../../src/databases/entities/InstalledPackages';
|
||||
import { InstalledNodes } from '../../src/databases/entities/InstalledNodes';
|
||||
import { randomName } from '../integration/shared/random';
|
||||
import config from '../../config';
|
||||
import { installedPackagePayload, installedNodePayload } from '../integration/shared/utils';
|
||||
|
||||
describe('parsePackageName', () => {
|
||||
it('Should fail with empty package name', () => {
|
||||
expect(() => parsePackageName('')).toThrowError()
|
||||
});
|
||||
import type { CommunityPackages } from '../../src/Interfaces';
|
||||
|
||||
it('Should fail with invalid package prefix name', () => {
|
||||
expect(() => parsePackageName('INVALID_PREFIX@123')).toThrowError()
|
||||
});
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('child_process');
|
||||
jest.mock('axios');
|
||||
|
||||
it('Should parse valid package name', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const parsedPackageName = parsePackageName(validPackageName);
|
||||
|
||||
expect(parsedPackageName.originalString).toBe(validPackageName);
|
||||
expect(parsedPackageName.packageName).toBe(validPackageName);
|
||||
expect(parsedPackageName.scope).toBeUndefined();
|
||||
expect(parsedPackageName.version).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should parse valid package name and version', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageName}@${validPackageVersion}`;
|
||||
const parsedPackageName = parsePackageName(fullPackageName);
|
||||
|
||||
expect(parsedPackageName.originalString).toBe(fullPackageName);
|
||||
expect(parsedPackageName.packageName).toBe(validPackageName);
|
||||
expect(parsedPackageName.scope).toBeUndefined();
|
||||
expect(parsedPackageName.version).toBe(validPackageVersion);
|
||||
});
|
||||
|
||||
it('Should parse valid package name, scope and version', () => {
|
||||
const validPackageScope = '@n8n';
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageScope}/${validPackageName}@${validPackageVersion}`;
|
||||
const parsedPackageName = parsePackageName(fullPackageName);
|
||||
|
||||
expect(parsedPackageName.originalString).toBe(fullPackageName);
|
||||
expect(parsedPackageName.packageName).toBe(`${validPackageScope}/${validPackageName}`);
|
||||
expect(parsedPackageName.scope).toBe(validPackageScope);
|
||||
expect(parsedPackageName.version).toBe(validPackageVersion);
|
||||
});
|
||||
describe('parsePackageName', () => {
|
||||
test('Should fail with empty package name', () => {
|
||||
expect(() => parseNpmPackageName('')).toThrowError();
|
||||
});
|
||||
|
||||
describe('executeCommand', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fsAccess.mockReset();
|
||||
// @ts-ignore
|
||||
fsMkdir.mockReset();
|
||||
// @ts-ignore
|
||||
exec.mockReset();
|
||||
});
|
||||
|
||||
it('Should call command with valid options', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
expect(args[1].cwd).toBeDefined();
|
||||
expect(args[1].env).toBeDefined();
|
||||
// PATH or NODE_PATH may be undefined depending on environment so we don't check for these keys.
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it ('Should make sure folder exists', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it ('Should try to create folder if it does not exist', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
fsAccess.mockImplementation(() => {
|
||||
throw new Error('Folder does not exist.');
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should throw especial error when package is not found', async() => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(new Error('Something went wrong - ' + NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR + '. Aborting.'));
|
||||
});
|
||||
|
||||
await expect(async () => await executeCommand('ls')).rejects.toThrow(RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND);
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
test('Should fail with invalid package prefix name', () => {
|
||||
expect(() => parseNpmPackageName('INVALID_PREFIX@123')).toThrowError();
|
||||
});
|
||||
|
||||
test('Should parse valid package name', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const parsed = parseNpmPackageName(validPackageName);
|
||||
|
||||
describe('crossInformationPackage', () => {
|
||||
|
||||
it('Should return same list if availableUpdates is undefined', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages);
|
||||
expect(crossedData).toEqual(fakePackages);
|
||||
});
|
||||
|
||||
it ('Should correctly match update versions for packages', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: NpmUpdatesAvailable = {
|
||||
[fakePackages[0].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.2.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
}
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBe('0.2.0');
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
|
||||
});
|
||||
|
||||
it ('Should correctly match update versions for single package', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: NpmUpdatesAvailable = {
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
}
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBeUndefined();
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
|
||||
});
|
||||
|
||||
expect(parsed.rawString).toBe(validPackageName);
|
||||
expect(parsed.packageName).toBe(validPackageName);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('matchMissingPackages', () => {
|
||||
it('Should not match failed packages that do not exist', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${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 = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
test('Should parse valid package name and version', () => {
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageName}@${validPackageVersion}`;
|
||||
const parsed = parseNpmPackageName(fullPackageName);
|
||||
|
||||
expect(matchedPackages).toEqual(fakePackages);
|
||||
expect(matchedPackages[0].failedLoading).toBeUndefined();
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should match failed packages that should be present', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@${fakePackages[0].installedVersion}`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should match failed packages even if version is wrong', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@123.456.789`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(validPackageName);
|
||||
expect(parsed.scope).toBeUndefined();
|
||||
expect(parsed.version).toBe(validPackageVersion);
|
||||
});
|
||||
|
||||
describe('checkPackageStatus', () => {
|
||||
it('Should call axios.post', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
await checkPackageStatus(packageName);
|
||||
expect(axios.post).toHaveBeenCalled();
|
||||
});
|
||||
test('Should parse valid package name, scope and version', () => {
|
||||
const validPackageScope = '@n8n';
|
||||
const validPackageName = NODE_PACKAGE_PREFIX + 'cool-package-name';
|
||||
const validPackageVersion = '0.1.1';
|
||||
const fullPackageName = `${validPackageScope}/${validPackageName}@${validPackageVersion}`;
|
||||
const parsed = parseNpmPackageName(fullPackageName);
|
||||
|
||||
it('Should not fail if request fails', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
axios.post = jest.fn(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
const result = await checkPackageStatus(packageName);
|
||||
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
||||
});
|
||||
expect(parsed.rawString).toBe(fullPackageName);
|
||||
expect(parsed.packageName).toBe(`${validPackageScope}/${validPackageName}`);
|
||||
expect(parsed.scope).toBe(validPackageScope);
|
||||
expect(parsed.version).toBe(validPackageVersion);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should warn if package is banned', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
// @ts-ignore
|
||||
axios.post = jest.fn(() => {
|
||||
return { data: { status: 'Banned', reason: 'Not good' } };
|
||||
});
|
||||
const result = await checkPackageStatus(packageName);
|
||||
expect(result.status).toBe('Banned');
|
||||
expect(result.reason).toBe('Not good');
|
||||
});
|
||||
describe('executeCommand', () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
fsAccess.mockReset();
|
||||
// @ts-ignore
|
||||
fsMkdir.mockReset();
|
||||
// @ts-ignore
|
||||
exec.mockReset();
|
||||
});
|
||||
|
||||
describe('hasPackageLoadedSuccessfully', () => {
|
||||
it('Should return true when failed package list does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
const result = hasPackageLoadedSuccessfully('package');
|
||||
expect(result).toBe(true);
|
||||
test('Should call command with valid options', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
expect(args[1].cwd).toBeDefined();
|
||||
expect(args[1].env).toBeDefined();
|
||||
// PATH or NODE_PATH may be undefined depending on environment so we don't check for these keys.
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
it('Should return true when package is not in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoadedSuccessfully('packageC');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('Should return false when package is in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoadedSuccessfully('packageA');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('removePackageFromMissingList', () => {
|
||||
it('Should do nothing if key does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
|
||||
removePackageFromMissingList('packageA');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBeUndefined();
|
||||
test('Should make sure folder exists', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
it('Should remove only correct package from list', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0');
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
removePackageFromMissingList('packageB');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe('packageA@0.1.0 packageBB@0.2.0');
|
||||
test('Should try to create folder if it does not exist', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(null, { stdout: 'Done' });
|
||||
});
|
||||
|
||||
|
||||
it('Should not remove if package is not in the list', () => {
|
||||
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0';
|
||||
config.set('nodes.packagesMissing', failedToLoadList);
|
||||
|
||||
removePackageFromMissingList('packageC');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe(failedToLoadList);
|
||||
// @ts-ignore
|
||||
fsAccess.mockImplementation(() => {
|
||||
throw new Error('Folder does not exist.');
|
||||
});
|
||||
|
||||
await executeCommand('ls');
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should throw especial error when package is not found', async () => {
|
||||
// @ts-ignore
|
||||
exec.mockImplementation((...args) => {
|
||||
const callbackFunction = args[args.length - 1];
|
||||
callbackFunction(
|
||||
new Error(
|
||||
'Something went wrong - ' +
|
||||
NPM_COMMAND_TOKENS.NPM_PACKAGE_NOT_FOUND_ERROR +
|
||||
'. Aborting.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
await expect(async () => await executeCommand('ls')).rejects.toThrow(
|
||||
RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND,
|
||||
);
|
||||
|
||||
expect(fsAccess).toHaveBeenCalled();
|
||||
expect(exec).toHaveBeenCalled();
|
||||
expect(fsMkdir).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('crossInformationPackage', () => {
|
||||
test('Should return same list if availableUpdates is undefined', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages);
|
||||
expect(crossedData).toEqual(fakePackages);
|
||||
});
|
||||
|
||||
test('Should correctly match update versions for packages', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[fakePackages[0].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.2.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBe('0.2.0');
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
|
||||
test('Should correctly match update versions for single package', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
|
||||
const updates: CommunityPackages.AvailableUpdates = {
|
||||
[fakePackages[1].packageName]: {
|
||||
current: fakePackages[0].installedVersion,
|
||||
wanted: fakePackages[0].installedVersion,
|
||||
latest: '0.3.0',
|
||||
location: fakePackages[0].packageName,
|
||||
},
|
||||
};
|
||||
|
||||
const crossedData = matchPackagesWithUpdates(fakePackages, updates);
|
||||
|
||||
// @ts-ignore
|
||||
expect(crossedData[0].updateAvailable).toBeUndefined();
|
||||
// @ts-ignore
|
||||
expect(crossedData[1].updateAvailable).toBe('0.3.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchMissingPackages', () => {
|
||||
test('Should not match failed packages that do not exist', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${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 = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages).toEqual(fakePackages);
|
||||
expect(matchedPackages[0].failedLoading).toBeUndefined();
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should match failed packages that should be present', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@${fakePackages[0].installedVersion}`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should match failed packages even if version is wrong', () => {
|
||||
const fakePackages = generateListOfFakeInstalledPackages();
|
||||
const notFoundPackageList = `${NODE_PACKAGE_PREFIX}very-long-name-that-should-never-be-generated@1.0.0 ${fakePackages[0].packageName}@123.456.789`;
|
||||
const matchedPackages = matchMissingPackages(fakePackages, notFoundPackageList);
|
||||
|
||||
expect(matchedPackages[0].failedLoading).toBe(true);
|
||||
expect(matchedPackages[1].failedLoading).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkNpmPackageStatus', () => {
|
||||
test('Should call axios.post', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
await checkNpmPackageStatus(packageName);
|
||||
expect(axios.post).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should not fail if request fails', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
axios.post = jest.fn(() => {
|
||||
throw new Error('Something went wrong');
|
||||
});
|
||||
const result = await checkNpmPackageStatus(packageName);
|
||||
expect(result.status).toBe(NPM_PACKAGE_STATUS_GOOD);
|
||||
});
|
||||
|
||||
test('Should warn if package is banned', async () => {
|
||||
const packageName = NODE_PACKAGE_PREFIX + randomName();
|
||||
// @ts-ignore
|
||||
axios.post = jest.fn(() => {
|
||||
return { data: { status: 'Banned', reason: 'Not good' } };
|
||||
});
|
||||
const result = await checkNpmPackageStatus(packageName);
|
||||
expect(result.status).toBe('Banned');
|
||||
expect(result.reason).toBe('Not good');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPackageLoadedSuccessfully', () => {
|
||||
test('Should return true when failed package list does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
const result = hasPackageLoaded('package');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return true when package is not in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoaded('packageC');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('Should return false when package is in the list of missing packages', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packgeB@0.1.0');
|
||||
const result = hasPackageLoaded('packageA');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removePackageFromMissingList', () => {
|
||||
test('Should do nothing if key does not exist', () => {
|
||||
config.set('nodes.packagesMissing', undefined);
|
||||
|
||||
removePackageFromMissingList('packageA');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBeUndefined();
|
||||
});
|
||||
|
||||
test('Should remove only correct package from list', () => {
|
||||
config.set('nodes.packagesMissing', 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0');
|
||||
|
||||
removePackageFromMissingList('packageB');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe('packageA@0.1.0 packageBB@0.2.0');
|
||||
});
|
||||
|
||||
test('Should not remove if package is not in the list', () => {
|
||||
const failedToLoadList = 'packageA@0.1.0 packageB@0.2.0 packageBB@0.2.0';
|
||||
config.set('nodes.packagesMissing', failedToLoadList);
|
||||
|
||||
removePackageFromMissingList('packageC');
|
||||
|
||||
const packageList = config.get('nodes.packagesMissing');
|
||||
expect(packageList).toBe(failedToLoadList);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates a list with 2 packages, one with a single node and
|
||||
* another with 2 nodes
|
||||
* @returns
|
||||
* Generate a list with 2 packages, one with a single node and another with 2 nodes
|
||||
*/
|
||||
function generateListOfFakeInstalledPackages(): InstalledPackages[] {
|
||||
const fakeInstalledPackage1 = new InstalledPackages();
|
||||
Object.assign(fakeInstalledPackage1, installedPackagePayload());
|
||||
|
||||
const fakeInstalledNode1 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode1, installedNodePayload(fakeInstalledPackage1.packageName));
|
||||
|
||||
fakeInstalledPackage1.installedNodes = [fakeInstalledNode1];
|
||||
|
||||
const fakeInstalledPackage2 = new InstalledPackages();
|
||||
Object.assign(fakeInstalledPackage2, installedPackagePayload());
|
||||
|
||||
const fakeInstalledNode2 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode2, installedNodePayload(fakeInstalledPackage2.packageName));
|
||||
|
||||
const fakeInstalledNode3 = new InstalledNodes();
|
||||
Object.assign(fakeInstalledNode3, installedNodePayload(fakeInstalledPackage2.packageName));
|
||||
|
||||
fakeInstalledPackage2.installedNodes = [fakeInstalledNode2, fakeInstalledNode3];
|
||||
|
||||
return [fakeInstalledPackage1, fakeInstalledPackage2];
|
||||
|
|
Loading…
Reference in a new issue