feat(core): Transfer folder structure when deleting a project (no-changelog) (#13865)

This commit is contained in:
Ricardo Espinoza
2025-03-12 09:54:13 -04:00
committed by GitHub
parent 2275b1780a
commit f760d4f21f
3 changed files with 51 additions and 5 deletions

View File

@@ -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 [];

View File

@@ -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.
}

View File

@@ -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,