mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Add tool to uninstall a community node (#14026)
This commit is contained in:
272
packages/cli/src/commands/__tests__/community-node.test.ts
Normal file
272
packages/cli/src/commands/__tests__/community-node.test.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import { type Config } from '@oclif/core';
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import { type CredentialsEntity } from '@/databases/entities/credentials-entity';
|
||||||
|
import { type InstalledNodes } from '@/databases/entities/installed-nodes';
|
||||||
|
import { type User } from '@/databases/entities/user';
|
||||||
|
|
||||||
|
import { CommunityNode } from '../community-node';
|
||||||
|
|
||||||
|
describe('uninstallCredential', () => {
|
||||||
|
const userId = '1234';
|
||||||
|
|
||||||
|
const config: Config = mock<Config>();
|
||||||
|
const communityNode = new CommunityNode(['--uninstall', '--credential', 'evolutionApi'], config);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityNode.deleteCredential = jest.fn();
|
||||||
|
communityNode.findCredentialsByType = jest.fn();
|
||||||
|
communityNode.findUserById = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a credential', async () => {
|
||||||
|
const credentialType = 'evolutionApi';
|
||||||
|
|
||||||
|
const credential = mock<CredentialsEntity>();
|
||||||
|
credential.id = '666';
|
||||||
|
|
||||||
|
const user = mock<User>();
|
||||||
|
const credentials = [credential];
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { credential: credentialType, uninstall: true, userId },
|
||||||
|
});
|
||||||
|
communityNode.findCredentialsByType = jest.fn().mockReturnValue(credentials);
|
||||||
|
communityNode.findUserById = jest.fn().mockReturnValue(user);
|
||||||
|
|
||||||
|
const deleteCredential = jest.spyOn(communityNode, 'deleteCredential');
|
||||||
|
const findCredentialsByType = jest.spyOn(communityNode, 'findCredentialsByType');
|
||||||
|
const findUserById = jest.spyOn(communityNode, 'findUserById');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledWith(credentialType);
|
||||||
|
|
||||||
|
expect(findUserById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findUserById).toHaveBeenCalledWith(userId);
|
||||||
|
|
||||||
|
expect(deleteCredential).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deleteCredential).toHaveBeenCalledWith(user, credential.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if the user is not found', async () => {
|
||||||
|
const credentialType = 'evolutionApi';
|
||||||
|
|
||||||
|
const credential = mock<CredentialsEntity>();
|
||||||
|
credential.id = '666';
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { credential: credentialType, uninstall: true, userId },
|
||||||
|
});
|
||||||
|
communityNode.findUserById = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const deleteCredential = jest.spyOn(communityNode, 'deleteCredential');
|
||||||
|
const findCredentialsByType = jest.spyOn(communityNode, 'findCredentialsByType');
|
||||||
|
const findUserById = jest.spyOn(communityNode, 'findUserById');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findUserById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findUserById).toHaveBeenCalledWith(userId);
|
||||||
|
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledTimes(0);
|
||||||
|
expect(deleteCredential).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if the credential is not found', async () => {
|
||||||
|
const credentialType = 'evolutionApi';
|
||||||
|
|
||||||
|
const credential = mock<CredentialsEntity>();
|
||||||
|
credential.id = '666';
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { credential: credentialType, uninstall: true, userId },
|
||||||
|
});
|
||||||
|
communityNode.findUserById = jest.fn().mockReturnValue(mock<User>());
|
||||||
|
communityNode.findCredentialsByType = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const deleteCredential = jest.spyOn(communityNode, 'deleteCredential');
|
||||||
|
const findCredentialsByType = jest.spyOn(communityNode, 'findCredentialsByType');
|
||||||
|
const findUserById = jest.spyOn(communityNode, 'findUserById');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findUserById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findUserById).toHaveBeenCalledWith(userId);
|
||||||
|
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledWith(credentialType);
|
||||||
|
|
||||||
|
expect(deleteCredential).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete multiple credentials', async () => {
|
||||||
|
const credentialType = 'evolutionApi';
|
||||||
|
|
||||||
|
const credential1 = mock<CredentialsEntity>();
|
||||||
|
credential1.id = '666';
|
||||||
|
|
||||||
|
const credential2 = mock<CredentialsEntity>();
|
||||||
|
credential2.id = '777';
|
||||||
|
|
||||||
|
const user = mock<User>();
|
||||||
|
const credentials = [credential1, credential2];
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { credential: credentialType, uninstall: true, userId },
|
||||||
|
});
|
||||||
|
communityNode.findCredentialsByType = jest.fn().mockReturnValue(credentials);
|
||||||
|
communityNode.findUserById = jest.fn().mockReturnValue(user);
|
||||||
|
|
||||||
|
const deleteCredential = jest.spyOn(communityNode, 'deleteCredential');
|
||||||
|
const findCredentialsByType = jest.spyOn(communityNode, 'findCredentialsByType');
|
||||||
|
const findUserById = jest.spyOn(communityNode, 'findUserById');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCredentialsByType).toHaveBeenCalledWith(credentialType);
|
||||||
|
|
||||||
|
expect(findUserById).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findUserById).toHaveBeenCalledWith(userId);
|
||||||
|
|
||||||
|
expect(deleteCredential).toHaveBeenCalledTimes(2);
|
||||||
|
expect(deleteCredential).toHaveBeenCalledWith(user, credential1.id);
|
||||||
|
expect(deleteCredential).toHaveBeenCalledWith(user, credential2.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('uninstallPackage', () => {
|
||||||
|
const config: Config = mock<Config>();
|
||||||
|
const communityNode = new CommunityNode(
|
||||||
|
['--uninstall', '--package', 'n8n-nodes-evolution-api.evolutionApi'],
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityNode.removeCommunityPackage = jest.fn();
|
||||||
|
communityNode.deleteCommunityNode = jest.fn();
|
||||||
|
communityNode.pruneDependencies = jest.fn();
|
||||||
|
communityNode.findCommunityPackage = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uninstall the package', async () => {
|
||||||
|
const installedNode = mock<InstalledNodes>();
|
||||||
|
const communityPackage = {
|
||||||
|
installedNodes: [installedNode],
|
||||||
|
};
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||||
|
});
|
||||||
|
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||||
|
|
||||||
|
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||||
|
const removeCommunityPackageSpy = jest.spyOn(communityNode, 'removeCommunityPackage');
|
||||||
|
const findCommunityPackage = jest.spyOn(communityNode, 'findCommunityPackage');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledWith('n8n-nodes-evolution-api');
|
||||||
|
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledWith(installedNode);
|
||||||
|
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledWith(
|
||||||
|
'n8n-nodes-evolution-api',
|
||||||
|
communityPackage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uninstall all nodes from a package', async () => {
|
||||||
|
const installedNode0 = mock<InstalledNodes>();
|
||||||
|
const installedNode1 = mock<InstalledNodes>();
|
||||||
|
|
||||||
|
const communityPackage = {
|
||||||
|
installedNodes: [installedNode0, installedNode1],
|
||||||
|
};
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||||
|
});
|
||||||
|
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||||
|
|
||||||
|
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||||
|
const removeCommunityPackageSpy = jest.spyOn(communityNode, 'removeCommunityPackage');
|
||||||
|
const findCommunityPackage = jest.spyOn(communityNode, 'findCommunityPackage');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledWith('n8n-nodes-evolution-api');
|
||||||
|
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledTimes(2);
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledWith(installedNode0);
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledWith(installedNode1);
|
||||||
|
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledWith(
|
||||||
|
'n8n-nodes-evolution-api',
|
||||||
|
communityPackage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if a package is not found', async () => {
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||||
|
});
|
||||||
|
communityNode.findCommunityPackage = jest.fn().mockReturnValue(null);
|
||||||
|
|
||||||
|
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||||
|
const removeCommunityPackageSpy = jest.spyOn(communityNode, 'removeCommunityPackage');
|
||||||
|
const findCommunityPackage = jest.spyOn(communityNode, 'findCommunityPackage');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledWith('n8n-nodes-evolution-api');
|
||||||
|
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if nodes are not found', async () => {
|
||||||
|
const communityPackage = {
|
||||||
|
installedNodes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
communityNode.parseFlags = jest.fn().mockReturnValue({
|
||||||
|
flags: { package: 'n8n-nodes-evolution-api', uninstall: true },
|
||||||
|
});
|
||||||
|
communityNode.findCommunityPackage = jest.fn().mockReturnValue(communityPackage);
|
||||||
|
|
||||||
|
const deleteCommunityNode = jest.spyOn(communityNode, 'deleteCommunityNode');
|
||||||
|
const removeCommunityPackageSpy = jest.spyOn(communityNode, 'removeCommunityPackage');
|
||||||
|
const findCommunityPackage = jest.spyOn(communityNode, 'findCommunityPackage');
|
||||||
|
|
||||||
|
await communityNode.run();
|
||||||
|
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(findCommunityPackage).toHaveBeenCalledWith('n8n-nodes-evolution-api');
|
||||||
|
|
||||||
|
expect(deleteCommunityNode).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(removeCommunityPackageSpy).toHaveBeenCalledWith(
|
||||||
|
'n8n-nodes-evolution-api',
|
||||||
|
communityPackage,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
166
packages/cli/src/commands/community-node.ts
Normal file
166
packages/cli/src/commands/community-node.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { Container } from '@n8n/di';
|
||||||
|
import { Flags } from '@oclif/core';
|
||||||
|
|
||||||
|
import { CredentialsService } from '@/credentials/credentials.service';
|
||||||
|
import { type InstalledNodes } from '@/databases/entities/installed-nodes';
|
||||||
|
import { type InstalledPackages } from '@/databases/entities/installed-packages';
|
||||||
|
import { type User } from '@/databases/entities/user';
|
||||||
|
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
||||||
|
import { InstalledNodesRepository } from '@/databases/repositories/installed-nodes.repository';
|
||||||
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
import { CommunityPackagesService } from '@/services/community-packages.service';
|
||||||
|
|
||||||
|
import { BaseCommand } from './base-command';
|
||||||
|
|
||||||
|
export class CommunityNode extends BaseCommand {
|
||||||
|
static description = '\nUninstall a community node and its credentials';
|
||||||
|
|
||||||
|
static examples = [
|
||||||
|
'$ n8n community-node --uninstall --package n8n-nodes-evolution-api',
|
||||||
|
'$ n8n community-node --uninstall --credential evolutionApi --userId 1234',
|
||||||
|
];
|
||||||
|
|
||||||
|
static flags = {
|
||||||
|
help: Flags.help({ char: 'h' }),
|
||||||
|
uninstall: Flags.boolean({
|
||||||
|
description: 'Uninstalls the node',
|
||||||
|
}),
|
||||||
|
package: Flags.string({
|
||||||
|
description: 'Package name of the community node.',
|
||||||
|
}),
|
||||||
|
credential: Flags.string({
|
||||||
|
description:
|
||||||
|
"Type of the credential.\nGet this value by visiting the node's .credential.ts file and getting the value of `name`",
|
||||||
|
}),
|
||||||
|
userId: Flags.string({
|
||||||
|
description:
|
||||||
|
'The ID of the user who owns the credential.\nOn self-hosted, query the database.\nOn cloud, query the API with your API key',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await super.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const { flags } = await this.parseFlags();
|
||||||
|
|
||||||
|
const packageName = flags.package;
|
||||||
|
const credentialType = flags.credential;
|
||||||
|
const userId = flags.userId;
|
||||||
|
|
||||||
|
if (!flags) {
|
||||||
|
this.logger.info('Please set flags. See help for more information.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.uninstall) {
|
||||||
|
this.logger.info('"--uninstall" has to be set!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packageName && !credentialType) {
|
||||||
|
this.logger.info('"--package" or "--credential" has to be set!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageName) {
|
||||||
|
await this.uninstallPackage(packageName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialType && userId) {
|
||||||
|
await this.uninstallCredential(credentialType, userId);
|
||||||
|
} else {
|
||||||
|
this.logger.info('"--userId" has to be set!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async catch(error: Error) {
|
||||||
|
this.logger.error('Error in node command:');
|
||||||
|
this.logger.error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uninstallCredential(credentialType: string, userId: string) {
|
||||||
|
const user = await this.findUserById(userId);
|
||||||
|
|
||||||
|
if (user === null) {
|
||||||
|
this.logger.info(`User ${userId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await this.findCredentialsByType(credentialType);
|
||||||
|
|
||||||
|
if (credentials === null) {
|
||||||
|
this.logger.info(`Credentials with type ${credentialType} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials.forEach(async (credential) => {
|
||||||
|
await this.deleteCredential(user, credential.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.info(`All credentials with type ${credentialType} successfully uninstalled`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserById(userId: string) {
|
||||||
|
return await Container.get(UserRepository).findOneBy({ id: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCredentialsByType(credentialType: string) {
|
||||||
|
return await Container.get(CredentialsRepository).findBy({ type: credentialType });
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCredential(user: User, credentialId: string) {
|
||||||
|
return await Container.get(CredentialsService).delete(user, credentialId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uninstallPackage(packageName: string) {
|
||||||
|
const communityPackage = await this.findCommunityPackage(packageName);
|
||||||
|
|
||||||
|
if (communityPackage === null) {
|
||||||
|
this.logger.info(`Package ${packageName} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.removeCommunityPackage(packageName, communityPackage);
|
||||||
|
|
||||||
|
const installedNodes = communityPackage?.installedNodes;
|
||||||
|
|
||||||
|
if (!installedNodes) {
|
||||||
|
this.logger.info(`Nodes in ${packageName} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of installedNodes) {
|
||||||
|
await this.deleteCommunityNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.pruneDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
async pruneDependencies() {
|
||||||
|
await Container.get(CommunityPackagesService).executeNpmCommand('npm prune');
|
||||||
|
}
|
||||||
|
|
||||||
|
async parseFlags() {
|
||||||
|
return await this.parse(CommunityNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCommunityNode(node: InstalledNodes) {
|
||||||
|
return await Container.get(InstalledNodesRepository).delete({
|
||||||
|
type: node.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCommunityPackage(packageName: string, communityPackage: InstalledPackages) {
|
||||||
|
return await Container.get(CommunityPackagesService).removePackage(
|
||||||
|
packageName,
|
||||||
|
communityPackage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCommunityPackage(packageName: string) {
|
||||||
|
return await Container.get(CommunityPackagesService).findInstalledPackage(packageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user