diff --git a/packages/cli/src/services/folder.service.ts b/packages/cli/src/services/folder.service.ts index 6f09e49931..3b4a9f3696 100644 --- a/packages/cli/src/services/folder.service.ts +++ b/packages/cli/src/services/folder.service.ts @@ -148,6 +148,17 @@ export class FolderService { }); } + async transferFoldersToProject(fromProjectId: string, toProjectId: string) { + return await this.folderRepository.update( + { + homeProject: { id: fromProjectId }, + }, + { + homeProject: { id: toProjectId }, + }, + ); + } + private transformFolderPathToTree(flatPath: FolderPathRow[]): SimpleFolderNode[] { if (!flatPath || flatPath.length === 0) { return []; diff --git a/packages/cli/src/services/project.service.ee.ts b/packages/cli/src/services/project.service.ee.ts index 95306bddf7..4ebaf86a0a 100644 --- a/packages/cli/src/services/project.service.ee.ts +++ b/packages/cli/src/services/project.service.ee.ts @@ -61,6 +61,12 @@ export class ProjectService { ); } + private get folderService() { + return import('@/services/folder.service').then(({ FolderService }) => + Container.get(FolderService), + ); + } + async deleteProject( user: User, projectId: string, @@ -134,16 +140,22 @@ export class ProjectService { } } - // 3. delete shared credentials into this project + // 3. Move folders over to the target project, before deleting the project else cascading will delete workflows + if (targetProject) { + const folderService = await this.folderService; + await folderService.transferFoldersToProject(project.id, targetProject.id); + } + + // 4. delete shared credentials into this project // Cascading deletes take care of this. - // 4. delete shared workflows into this project + // 5. delete shared workflows into this project // Cascading deletes take care of this. - // 5. delete project + // 6. delete project await this.projectRepository.remove(project); - // 6. delete project relations + // 7. delete project relations // Cascading deletes take care of this. } diff --git a/packages/cli/test/integration/project.api.test.ts b/packages/cli/test/integration/project.api.test.ts index 5f7c7e1b27..8e59fcdb49 100644 --- a/packages/cli/test/integration/project.api.test.ts +++ b/packages/cli/test/integration/project.api.test.ts @@ -6,6 +6,7 @@ import { EntityNotFoundError } from '@n8n/typeorm'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import type { Project } from '@/databases/entities/project'; import type { GlobalRole } from '@/databases/entities/user'; +import { FolderRepository } from '@/databases/repositories/folder.repository'; import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; @@ -13,6 +14,7 @@ import { SharedWorkflowRepository } from '@/databases/repositories/shared-workfl import { getWorkflowById } from '@/public-api/v1/handlers/workflows/workflows.service'; import { CacheService } from '@/services/cache/cache.service'; import { RoleService } from '@/services/role.service'; +import { createFolder } from '@test-integration/db/folders'; import { getCredentialById, @@ -1048,7 +1050,7 @@ describe('DELETE /project/:projectId', () => { .expect(404); }); - test('migrates workflows and credentials to another project if `migrateToProject` is passed', async () => { + test('migrates folders, workflows and credentials to another project if `migrateToProject` is passed', async () => { // // ARRANGE // @@ -1071,6 +1073,11 @@ describe('DELETE /project/:projectId', () => { { project: otherProject, role: 'workflow:editor' }, ]); + await createFolder(projectToBeDeleted, { name: 'folder1' }); + await createFolder(projectToBeDeleted, { name: 'folder2' }); + await createFolder(targetProject, { name: 'folder1' }); + await createFolder(otherProject, { name: 'folder3' }); + // // ACT // @@ -1128,6 +1135,22 @@ describe('DELETE /project/:projectId', () => { role: 'credential:user', }), ).resolves.toBeDefined(); + + // folders are in the target project + const foldersInTargetProject = await Container.get(FolderRepository).findBy({ + homeProject: { id: targetProject.id }, + }); + + const foldersInDeletedProject = await Container.get(FolderRepository).findBy({ + homeProject: { id: projectToBeDeleted.id }, + }); + + expect(foldersInDeletedProject).toHaveLength(0); + + expect(foldersInTargetProject).toHaveLength(3); + expect(foldersInTargetProject.map((f) => f.name)).toEqual( + expect.arrayContaining(['folder1', 'folder1', 'folder2']), + ); }); // This test is testing behavior that is explicitly not enabled right now,