feat(core): Add endpoint to transfer folder to another project (no-changelog) (#15005)

This commit is contained in:
Ricardo Espinoza
2025-05-07 07:51:03 -04:00
committed by GitHub
parent 1122ee7ec9
commit 715127fa87
11 changed files with 1063 additions and 66 deletions

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { folderIdSchema } from '../../schemas/folder.schema';
export class TransferFolderBodyDto extends Z.class({
destinationProjectId: z.string(),
shareCredentials: z.array(z.string()).optional(),
destinationParentFolderId: folderIdSchema,
}) {}

View File

@@ -63,6 +63,7 @@ export { CreateFolderDto } from './folders/create-folder.dto';
export { UpdateFolderDto } from './folders/update-folder.dto';
export { DeleteFolderDto } from './folders/delete-folder.dto';
export { ListFolderQueryDto } from './folders/list-folder-query.dto';
export { TransferFolderBodyDto } from './folders/transfer-folder.dto';
export { ListInsightsWorkflowQueryDto } from './insights/list-workflow-query.dto';
export { InsightsDateFilterDto } from './insights/date-filter.dto';

View File

@@ -23,7 +23,7 @@ export const RESOURCES = {
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
folder: [...DEFAULT_OPERATIONS] as const,
folder: [...DEFAULT_OPERATIONS, 'move'] as const,
insights: ['list'] as const,
} as const;

View File

@@ -76,6 +76,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
'project:update',
'project:delete',
'insights:list',
'folder:move',
];
export const GLOBAL_ADMIN_SCOPES = GLOBAL_OWNER_SCOPES.concat();

View File

@@ -30,6 +30,7 @@ export const REGULAR_PROJECT_ADMIN_SCOPES: Scope[] = [
'folder:update',
'folder:delete',
'folder:list',
'folder:move',
];
export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
@@ -55,6 +56,7 @@ export const PERSONAL_PROJECT_OWNER_SCOPES: Scope[] = [
'folder:update',
'folder:delete',
'folder:list',
'folder:move',
];
export const PROJECT_EDITOR_SCOPES: Scope[] = [

View File

@@ -2,6 +2,7 @@ import {
CreateFolderDto,
DeleteFolderDto,
ListFolderQueryDto,
TransferFolderBodyDto,
UpdateFolderDto,
} from '@n8n/api-types';
import {
@@ -13,6 +14,8 @@ import {
Patch,
Delete,
Query,
Put,
Param,
} from '@n8n/decorators';
import { Response } from 'express';
import { UserError } from 'n8n-workflow';
@@ -21,13 +24,17 @@ import { FolderNotFoundError } from '@/errors/folder-not-found.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { ListQuery } from '@/requests';
import { AuthenticatedRequest } from '@/requests';
import type { ListQuery } from '@/requests';
import { FolderService } from '@/services/folder.service';
import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee';
@RestController('/projects/:projectId/folders')
export class ProjectController {
constructor(private readonly folderService: FolderService) {}
constructor(
private readonly folderService: FolderService,
private readonly enterpriseWorkflowService: EnterpriseWorkflowService,
) {}
@Post('/')
@ProjectScope('folder:create')
@@ -145,4 +152,23 @@ export class ProjectController {
throw new InternalServerError(undefined, e);
}
}
@Put('/:folderId/transfer')
@ProjectScope('folder:move')
async transferFolderToProject(
req: AuthenticatedRequest,
_res: unknown,
@Param('folderId') sourceFolderId: string,
@Param('projectId') sourceProjectId: string,
@Body body: TransferFolderBodyDto,
) {
return await this.enterpriseWorkflowService.transferFolder(
req.user,
sourceProjectId,
sourceFolderId,
body.destinationProjectId,
body.destinationParentFolderId,
body.shareCredentials,
);
}
}

View File

@@ -82,7 +82,7 @@ export = {
const body = z.object({ destinationProjectId: z.string() }).parse(req.body);
await Container.get(EnterpriseWorkflowService).transferOne(
await Container.get(EnterpriseWorkflowService).transferWorkflow(
req.user,
workflowId,
body.destinationProjectId,

View File

@@ -1,17 +1,18 @@
import type {
CredentialsEntity,
User,
WorkflowEntity,
WorkflowWithSharingsAndCredentials,
WorkflowWithSharingsMetaDataAndCredentials,
} from '@n8n/db';
import { Project, SharedWorkflow, CredentialsRepository } from '@n8n/db';
import { Folder, Project, SharedWorkflow, CredentialsRepository, FolderRepository } from '@n8n/db';
import { Service } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In, type EntityManager } from '@n8n/typeorm';
import omit from 'lodash/omit';
import { Logger } from 'n8n-core';
import type { IWorkflowBase, WorkflowId } from 'n8n-workflow';
import { NodeOperationError, UserError, WorkflowActivationError } from 'n8n-workflow';
import { NodeOperationError, PROJECT_ROOT, UserError, WorkflowActivationError } from 'n8n-workflow';
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
@@ -43,6 +44,7 @@ export class EnterpriseWorkflowService {
private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
private readonly workflowFinderService: WorkflowFinderService,
private readonly folderService: FolderService,
private readonly folderRepository: FolderRepository,
) {}
async shareWithProjects(
@@ -260,7 +262,7 @@ export class EnterpriseWorkflowService {
});
}
async transferOne(
async transferWorkflow(
user: User,
workflowId: string,
destinationProjectId: string,
@@ -326,70 +328,209 @@ export class EnterpriseWorkflowService {
}
// 7. transfer the workflow
await this.workflowRepository.manager.transaction(async (trx) => {
// remove all sharings
await trx.remove(workflow.shared);
// create new owner-sharing
await trx.save(
trx.create(SharedWorkflow, {
workflowId: workflow.id,
projectId: destinationProject.id,
role: 'workflow:owner',
}),
);
});
await this.transferWorkflowOwnership([workflow], destinationProject.id);
// 8. share credentials into the destination project
await this.workflowRepository.manager.transaction(async (trx) => {
const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser(
user,
['credential:share'],
trx,
);
const credentialsAllowedToShare = allCredentials.filter((c) =>
shareCredentials.includes(c.id),
);
for (const credential of credentialsAllowedToShare) {
await this.enterpriseCredentialsService.shareWithProjects(
user,
credential.id,
[destinationProject.id],
trx,
);
}
});
await this.shareCredentialsWithProject(user, shareCredentials, destinationProject.id);
// 9. Move workflow to the right folder if any
await this.workflowRepository.update({ id: workflow.id }, { parentFolder });
// 10. try to activate it again if it was active
if (wasActive) {
try {
await this.activeWorkflowManager.add(workflowId, 'update');
return;
} catch (error) {
await this.workflowRepository.updateActiveState(workflowId, false);
// Since the transfer worked we return a 200 but also return the
// activation error as data.
if (error instanceof WorkflowActivationError) {
return {
error: error.toJSON
? error.toJSON()
: {
name: error.name,
message: error.message,
},
};
}
throw error;
}
return await this.attemptWorkflowReactivation(workflowId);
}
return;
}
async transferFolder(
user: User,
sourceProjectId: string,
sourceFolderId: string,
destinationProjectId: string,
destinationParentFolderId: string,
shareCredentials: string[] = [],
) {
// 1. Get all children folders
const childrenFolderIds = await this.folderRepository.getAllFolderIdsInHierarchy(
sourceFolderId,
sourceProjectId,
);
// 2. Get all workflows in the nested folders
const workflows = await this.workflowRepository.find({
select: ['id', 'active', 'shared'],
relations: ['shared', 'shared.project'],
where: {
parentFolder: { id: In([...childrenFolderIds, sourceFolderId]) },
},
});
const activeWorkflows = workflows.filter((w) => w.active).map((w) => w.id);
// 3. get destination project
const destinationProject = await this.projectService.getProjectWithScope(
user,
destinationProjectId,
['workflow:create'],
);
NotFoundError.isDefinedAndNotNull(
destinationProject,
`Could not find project with the id "${destinationProjectId}". Make sure you have the permission to create workflows in it.`,
);
// 4. checks
if (destinationParentFolderId !== PROJECT_ROOT) {
await this.folderRepository.findOneOrFailFolderInProject(
destinationParentFolderId,
destinationProjectId,
);
}
await this.folderRepository.findOneOrFailFolderInProject(sourceFolderId, sourceProjectId);
for (const workflow of workflows) {
const ownerSharing = workflow.shared.find((s) => s.role === 'workflow:owner')!;
NotFoundError.isDefinedAndNotNull(
ownerSharing,
`Could not find owner for workflow "${workflow.id}"`,
);
const sourceProject = ownerSharing.project;
if (sourceProject.id === destinationProject.id) {
throw new TransferWorkflowError(
"You can't transfer a workflow into the project that's already owning it.",
);
}
}
// 5. deactivate all workflows if necessary
const deactivateWorkflowsPromises = activeWorkflows.map(
async (workflowId) => await this.activeWorkflowManager.remove(workflowId),
);
await Promise.all(deactivateWorkflowsPromises);
// 6. transfer the workflows
await this.transferWorkflowOwnership(workflows, destinationProject.id);
// 7. share credentials into the destination project
await this.shareCredentialsWithProject(user, shareCredentials, destinationProject.id);
// 8. Move all children folder to the destination project
await this.moveFoldersToDestination(
sourceFolderId,
childrenFolderIds,
destinationProjectId,
destinationParentFolderId,
);
// 9. try to activate workflows again if they were active
for (const workflowId of activeWorkflows) {
await this.attemptWorkflowReactivation(workflowId);
}
}
private formatActivationError(error: WorkflowActivationError) {
return {
error: error.toJSON
? error.toJSON()
: {
name: error.name,
message: error.message,
},
};
}
private async attemptWorkflowReactivation(workflowId: string) {
try {
await this.activeWorkflowManager.add(workflowId, 'update');
return;
} catch (error) {
await this.workflowRepository.updateActiveState(workflowId, false);
if (error instanceof WorkflowActivationError) {
return this.formatActivationError(error);
}
throw error;
}
}
private async transferWorkflowOwnership(
workflows: WorkflowEntity[],
destinationProjectId: string,
) {
await this.workflowRepository.manager.transaction(async (trx) => {
for (const workflow of workflows) {
// Remove all sharings
await trx.remove(workflow.shared);
// Create new owner-sharing
await trx.save(
trx.create(SharedWorkflow, {
workflowId: workflow.id,
projectId: destinationProjectId,
role: 'workflow:owner',
}),
);
}
});
}
private async shareCredentialsWithProject(
user: User,
credentialIds: string[],
projectId: string,
) {
await this.workflowRepository.manager.transaction(async (trx) => {
const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser(
user,
['credential:share'],
trx,
);
const credentialsToShare = allCredentials.filter((c) => credentialIds.includes(c.id));
for (const credential of credentialsToShare) {
await this.enterpriseCredentialsService.shareWithProjects(
user,
credential.id,
[projectId],
trx,
);
}
});
}
private async moveFoldersToDestination(
sourceFolderId: string,
childrenFolderIds: string[],
destinationProjectId: string,
destinationParentFolderId: string,
) {
await this.folderRepository.manager.transaction(async (trx) => {
// Move all children folders to the destination project
await trx.update(
Folder,
{ id: In(childrenFolderIds) },
{ homeProject: { id: destinationProjectId } },
);
// Move source folder to destination project and under destination folder if specified
await trx.update(
Folder,
{ id: sourceFolderId },
{
homeProject: { id: destinationProjectId },
parentFolder:
destinationParentFolderId === PROJECT_ROOT ? null : { id: destinationParentFolderId },
},
);
});
}
}

View File

@@ -548,7 +548,7 @@ export class WorkflowsController {
@Param('workflowId') workflowId: string,
@Body body: TransferWorkflowBodyDto,
) {
return await this.enterpriseWorkflowService.transferOne(
return await this.enterpriseWorkflowService.transferWorkflow(
req.user,
workflowId,
body.destinationProjectId,

View File

@@ -1,18 +1,27 @@
import type { Project } from '@n8n/db';
import type { Project, ProjectRole } from '@n8n/db';
import type { User } from '@n8n/db';
import { FolderRepository } from '@n8n/db';
import { ProjectRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import { DateTime } from 'luxon';
import { PROJECT_ROOT } from 'n8n-workflow';
import { ApplicationError, PROJECT_ROOT } from 'n8n-workflow';
import { ActiveWorkflowManager } from '@/active-workflow-manager';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { mockInstance } from '@test/mocking';
import {
getCredentialSharings,
saveCredential,
shareCredentialWithProjects,
shareCredentialWithUsers,
} from '@test-integration/db/credentials';
import { createFolder } from '@test-integration/db/folders';
import { createTag } from '@test-integration/db/tags';
import { createWorkflow } from '@test-integration/db/workflows';
import { createWorkflow, getWorkflowSharing } from '@test-integration/db/workflows';
import { randomCredentialPayload } from '@test-integration/random';
import { createTeamProject, getPersonalProject, linkUserToProject } from '../shared/db/projects';
import { createOwner, createMember } from '../shared/db/users';
import { createOwner, createMember, createUser, createAdmin } from '../shared/db/users';
import * as testDb from '../shared/test-db';
import type { SuperAgentTest } from '../shared/types';
import * as utils from '../shared/utils/';
@@ -23,6 +32,7 @@ let authOwnerAgent: SuperAgentTest;
let authMemberAgent: SuperAgentTest;
let ownerProject: Project;
let memberProject: Project;
let admin: User;
const testServer = utils.setupTestServer({
endpointGroups: ['folder'],
@@ -32,6 +42,8 @@ let projectRepository: ProjectRepository;
let folderRepository: FolderRepository;
let workflowRepository: WorkflowRepository;
const activeWorkflowManager = mockInstance(ActiveWorkflowManager);
beforeEach(async () => {
await testDb.truncate(['Folder', 'SharedWorkflow', 'TagEntity', 'Project', 'ProjectRelation']);
@@ -46,6 +58,7 @@ beforeEach(async () => {
ownerProject = await getPersonalProject(owner);
memberProject = await getPersonalProject(member);
admin = await createAdmin();
});
describe('POST /projects/:projectId/folders', () => {
@@ -1354,3 +1367,805 @@ describe('GET /projects/:projectId/folders/content', () => {
expect(response.body.data.totalSubFolders).toBe(2);
});
});
describe('PUT /projects/:projectId/folders/:folderId/transfer', () => {
test('cannot transfer into the same project', async () => {
const sourceProject = await createTeamProject('source project', member);
const destinationProject = await createTeamProject('Team Project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
await createWorkflow({ active: true, parentFolder: sourceFolder1 }, destinationProject);
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
.expect(400);
});
test('cannot transfer somebody elses folder', async () => {
const sourceProject = await createTeamProject('source project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
await createWorkflow({ parentFolder: sourceFolder1 }, owner);
const destinationProject = await createTeamProject('Team Project', admin);
const destinationFolder1 = await createFolder(destinationProject, { name: 'Source Folder 1' });
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: destinationFolder1,
})
.expect(400);
});
test("cannot transfer if you're not a member of the destination project", async () => {
const sourceProject = await getPersonalProject(member);
const destinationProject = await createTeamProject('Team Project', owner);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
await createWorkflow({ active: true }, destinationProject);
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
.expect(404);
});
test.each<ProjectRole>(['project:editor', 'project:viewer'])(
'%ss cannot transfer workflows',
async (projectRole) => {
//
// ARRANGE
//
const sourceProject = await createTeamProject();
await linkUserToProject(member, sourceProject, projectRole);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
await createWorkflow({}, sourceProject);
const destinationProject = await createTeamProject();
await linkUserToProject(member, destinationProject, 'project:admin');
//
// ACT & ASSERT
//
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
.expect(403);
},
);
test.each<
[
// user role
'owners' | 'admins',
// source project type
'team' | 'personal',
// destination project type
'team' | 'personal',
// actor
() => User,
// source project
() => Promise<Project> | Project,
// destination project
() => Promise<Project> | Project,
]
>([
// owner
[
'owners',
'team',
'team',
() => owner,
async () => await createTeamProject('Source Project'),
async () => await createTeamProject('Destination Project'),
],
[
'owners',
'team',
'personal',
() => owner,
async () => await createTeamProject('Source Project'),
() => memberProject,
],
[
'owners',
'personal',
'team',
() => owner,
() => memberProject,
async () => await createTeamProject('Destination Project'),
],
// admin
[
'admins',
'team',
'team',
() => admin,
async () => await createTeamProject('Source Project'),
async () => await createTeamProject('Destination Project'),
],
[
'admins',
'team',
'personal',
() => admin,
async () => await createTeamProject('Source Project'),
() => memberProject,
],
[
'admins',
'personal',
'team',
() => admin,
() => memberProject,
async () => await createTeamProject('Destination Project'),
],
])(
'global %s can transfer workflows from a %s project to a %s project',
async (
_roleName,
_sourceProjectName,
_destinationProjectName,
getActor,
getSourceProject,
getDestinationProject,
) => {
// ARRANGE
const actor = getActor();
const sourceProject = await getSourceProject();
const destinationProject = await getDestinationProject();
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const workflow = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
// ACT
const response = await testServer
.authAgentFor(actor)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({ destinationProjectId: destinationProject.id, destinationParentFolderId: '0' })
.expect(200);
// ASSERT
expect(response.body).toEqual({});
const allSharings = await getWorkflowSharing(workflow);
expect(allSharings).toHaveLength(1);
expect(allSharings[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow.id,
role: 'workflow:owner',
});
},
);
test('owner transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
// ARRANGE
const admin = await createUser({ role: 'global:admin' });
const sourceProject = await createTeamProject('source project', admin);
const destinationProject = await createTeamProject('destination project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
project: sourceProject,
role: 'credential:owner',
});
// ACT
await testServer
.authAgentFor(owner)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: destinationProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('admin transfers folder from project they are not part of, e.g. test global cred sharing scope', async () => {
// ARRANGE
const admin = await createUser({ role: 'global:admin' });
const sourceProject = await createTeamProject('source project', owner);
const destinationProject = await createTeamProject('destination project', owner);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
project: sourceProject,
role: 'credential:owner',
});
// ACT
await testServer
.authAgentFor(admin)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: destinationProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('member transfers folder from personal project to team project and one workflow contains a credential that they can use but not share', async () => {
// ARRANGE
const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id);
const sourceProject = await projectRepository.getPersonalProjectForUserOrFail(member.id);
const destinationProject = await createTeamProject('destination project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
user: owner,
role: 'credential:owner',
});
await shareCredentialWithUsers(credential, [member]);
// ACT
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: ownerPersonalProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('member transfers folder from their personal project to another team project in which they have editor role', async () => {
// ARRANGE
const sourceProject = await projectRepository.getPersonalProjectForUserOrFail(member.id);
const destinationProject = await createTeamProject('destination project');
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
project: sourceProject,
role: 'credential:owner',
});
await linkUserToProject(member, destinationProject, 'project:editor');
// ACT
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: destinationProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('member transfers folder from a team project as project admin to another team project in which they have editor role', async () => {
// ARRANGE
const sourceProject = await createTeamProject('source project', member);
const destinationProject = await createTeamProject('destination project');
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
project: sourceProject,
role: 'credential:owner',
});
await linkUserToProject(member, destinationProject, 'project:editor');
// ACT
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: destinationProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share the credential that is only shared into the source project', async () => {
// ARRANGE
const sourceProject = await createTeamProject('source project', member);
const destinationProject = await createTeamProject('destination project');
const ownerProject = await getPersonalProject(owner);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
user: owner,
role: 'credential:owner',
});
await linkUserToProject(member, destinationProject, 'project:editor');
await shareCredentialWithProjects(credential, [sourceProject]);
// ACT
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: ownerProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('member transfers workflow from a team project as project admin to another team project in which they have editor role but cannot share all the credentials', async () => {
// ARRANGE
const sourceProject = await createTeamProject('source project', member);
const destinationProject = await createTeamProject('destination project');
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
const workflow1 = await createWorkflow({ parentFolder: sourceFolder1 }, sourceProject);
const workflow2 = await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
const credential = await saveCredential(randomCredentialPayload(), {
project: sourceProject,
role: 'credential:owner',
});
const ownersCredential = await saveCredential(randomCredentialPayload(), {
user: owner,
role: 'credential:owner',
});
await linkUserToProject(member, destinationProject, 'project:editor');
// ACT
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
shareCredentials: [credential.id, ownersCredential.id],
})
.expect(200);
// ASSERT
const workflow1Sharing = await getWorkflowSharing(workflow1);
expect(workflow1Sharing).toHaveLength(1);
expect(workflow1Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow1.id,
role: 'workflow:owner',
});
const workflow2Sharing = await getWorkflowSharing(workflow2);
expect(workflow2Sharing).toHaveLength(1);
expect(workflow2Sharing[0]).toMatchObject({
projectId: destinationProject.id,
workflowId: workflow2.id,
role: 'workflow:owner',
});
const sourceFolderInDb = await folderRepository.findOne({
where: { id: sourceFolder1.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolderInDb).toBeDefined();
expect(sourceFolderInDb?.parentFolder).toBeNull();
expect(sourceFolderInDb?.homeProject.id).toBe(destinationProject.id);
const sourceFolder2InDb = await folderRepository.findOne({
where: { id: sourceFolder2.id },
relations: ['parentFolder', 'homeProject'],
});
expect(sourceFolder2InDb).toBeDefined();
expect(sourceFolder2InDb?.parentFolder?.id).toBe(sourceFolder1.id);
expect(sourceFolder2InDb?.homeProject.id).toBe(destinationProject.id);
const allCredentialSharings = await getCredentialSharings(credential);
expect(allCredentialSharings).toHaveLength(2);
expect(allCredentialSharings).toEqual(
expect.arrayContaining([
expect.objectContaining({
projectId: sourceProject.id,
credentialsId: credential.id,
role: 'credential:owner',
}),
expect.objectContaining({
projectId: destinationProject.id,
credentialsId: credential.id,
role: 'credential:user',
}),
]),
);
});
test('returns a 500 if the workflow cannot be activated due to an unknown error', async () => {
//
// ARRANGE
//
const sourceProject = await createTeamProject('source project', member);
const destinationProject = await createTeamProject('Team Project', member);
const sourceFolder1 = await createFolder(sourceProject, { name: 'Source Folder 1' });
const sourceFolder2 = await createFolder(sourceProject, {
name: 'Source Folder 2',
parentFolder: sourceFolder1,
});
await createWorkflow({ active: true, parentFolder: sourceFolder1 }, sourceProject);
await createWorkflow({ parentFolder: sourceFolder2 }, sourceProject);
activeWorkflowManager.add.mockRejectedValue(new ApplicationError('Oh no!'));
//
// ACT & ASSERT
//
await testServer
.authAgentFor(member)
.put(`/projects/${sourceProject.id}/folders/${sourceFolder1.id}/transfer`)
.send({
destinationProjectId: destinationProject.id,
destinationParentFolderId: '0',
})
.expect(500);
});
});

View File

@@ -37,6 +37,7 @@ describe('EnterpriseWorkflowService', () => {
mock(),
mock(),
mock(),
mock(),
);
});