feat(core): Transfer folder structure when deleting user (no-changelog) (#13845)

This commit is contained in:
Ricardo Espinoza
2025-03-12 10:34:51 -04:00
committed by GitHub
parent d8bfc246b2
commit c7bcdc544d
6 changed files with 60 additions and 10 deletions

View File

@@ -25,6 +25,7 @@ describe('UsersController', () => {
mock(), mock(),
projectService, projectService,
eventService, eventService,
mock(),
); );
beforeEach(() => { beforeEach(() => {

View File

@@ -29,6 +29,7 @@ import { ExternalHooks } from '@/external-hooks';
import type { PublicUser } from '@/interfaces'; import type { PublicUser } from '@/interfaces';
import { listQueryMiddleware } from '@/middlewares'; import { listQueryMiddleware } from '@/middlewares';
import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests'; import { AuthenticatedRequest, ListQuery, UserRequest } from '@/requests';
import { FolderService } from '@/services/folder.service';
import { ProjectService } from '@/services/project.service.ee'; import { ProjectService } from '@/services/project.service.ee';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
import { WorkflowService } from '@/workflows/workflow.service'; import { WorkflowService } from '@/workflows/workflow.service';
@@ -48,6 +49,7 @@ export class UsersController {
private readonly credentialsService: CredentialsService, private readonly credentialsService: CredentialsService,
private readonly projectService: ProjectService, private readonly projectService: ProjectService,
private readonly eventService: EventService, private readonly eventService: EventService,
private readonly folderService: FolderService,
) {} ) {}
static ERROR_MESSAGES = { static ERROR_MESSAGES = {
@@ -215,6 +217,12 @@ export class UsersController {
transfereePersonalProject.id, transfereePersonalProject.id,
trx, trx,
); );
await this.folderService.transferAllFoldersToProject(
personalProjectToDelete.id,
transfereePersonalProject.id,
trx,
);
}); });
await this.projectService.clearCredentialCanUseExternalSecretsCache( await this.projectService.clearCredentialCanUseExternalSecretsCache(

View File

@@ -276,4 +276,21 @@ export class FolderRepository extends Repository<FolderWithWorkflowAndSubFolderC
}, },
); );
} }
async transferAllFoldersToProject(
fromProjectId: string,
toProjectId: string,
tx?: EntityManager,
) {
const manager = tx ?? this.manager;
return await manager.update(
Folder,
{
homeProject: { id: fromProjectId },
},
{
homeProject: { id: toProjectId },
},
);
}
} }

View File

@@ -148,15 +148,12 @@ export class FolderService {
}); });
} }
async transferFoldersToProject(fromProjectId: string, toProjectId: string) { async transferAllFoldersToProject(
return await this.folderRepository.update( fromProjectId: string,
{ toProjectId: string,
homeProject: { id: fromProjectId }, tx?: EntityManager,
}, ) {
{ return await this.folderRepository.transferAllFoldersToProject(fromProjectId, toProjectId, tx);
homeProject: { id: toProjectId },
},
);
} }
private transformFolderPathToTree(flatPath: FolderPathRow[]): SimpleFolderNode[] { private transformFolderPathToTree(flatPath: FolderPathRow[]): SimpleFolderNode[] {

View File

@@ -143,7 +143,7 @@ export class ProjectService {
// 3. Move folders over to the target project, before deleting the project else cascading will delete workflows // 3. Move folders over to the target project, before deleting the project else cascading will delete workflows
if (targetProject) { if (targetProject) {
const folderService = await this.folderService; const folderService = await this.folderService;
await folderService.transferFoldersToProject(project.id, targetProject.id); await folderService.transferAllFoldersToProject(project.id, targetProject.id);
} }
// 4. delete shared credentials into this project // 4. delete shared credentials into this project

View File

@@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { UsersController } from '@/controllers/users.controller'; import { UsersController } from '@/controllers/users.controller';
import type { User } from '@/databases/entities/user'; import type { User } from '@/databases/entities/user';
import { FolderRepository } from '@/databases/repositories/folder.repository';
import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository'; import { ProjectRelationRepository } from '@/databases/repositories/project-relation.repository';
import { ProjectRepository } from '@/databases/repositories/project.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository';
import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository'; import { SharedCredentialsRepository } from '@/databases/repositories/shared-credentials.repository';
@@ -12,6 +13,7 @@ import { UserRepository } from '@/databases/repositories/user.repository';
import { ExecutionService } from '@/executions/execution.service'; import { ExecutionService } from '@/executions/execution.service';
import { CacheService } from '@/services/cache/cache.service'; import { CacheService } from '@/services/cache/cache.service';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
import { createFolder } from '@test-integration/db/folders';
import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { import {
@@ -458,6 +460,13 @@ describe('DELETE /users/:id', () => {
getPersonalProject(transferee), getPersonalProject(transferee),
]); ]);
await Promise.all([
createFolder(memberPersonalProject, { name: 'folder1' }),
createFolder(memberPersonalProject, { name: 'folder2' }),
createFolder(transfereePersonalProject, { name: 'folder3' }),
createFolder(transfereePersonalProject, { name: 'folder1' }),
]);
const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany'); const deleteSpy = jest.spyOn(Container.get(CacheService), 'deleteMany');
// //
@@ -485,6 +494,7 @@ describe('DELETE /users/:id', () => {
const projectRelationRepository = Container.get(ProjectRelationRepository); const projectRelationRepository = Container.get(ProjectRelationRepository);
const sharedWorkflowRepository = Container.get(SharedWorkflowRepository); const sharedWorkflowRepository = Container.get(SharedWorkflowRepository);
const sharedCredentialsRepository = Container.get(SharedCredentialsRepository); const sharedCredentialsRepository = Container.get(SharedCredentialsRepository);
const folderRepository = Container.get(FolderRepository);
await Promise.all([ await Promise.all([
// user, their personal project and their relationship to the team project is gone // user, their personal project and their relationship to the team project is gone
@@ -574,6 +584,23 @@ describe('DELETE /users/:id', () => {
}), }),
).resolves.not.toBeNull(), ).resolves.not.toBeNull(),
]); ]);
// Assert that the folders have been transferred
const transfereeFolders = await folderRepository.findBy({
homeProject: { id: transfereePersonalProject.id },
});
const deletedUserFolders = await folderRepository.findBy({
homeProject: { id: memberPersonalProject.id },
});
expect(transfereeFolders).toHaveLength(4);
expect(transfereeFolders.map((folder) => folder.name)).toEqual(
expect.arrayContaining(['folder1', 'folder2', 'folder3', 'folder1']),
);
expect(deletedUserFolders).toHaveLength(0);
}); });
test('should fail to delete self', async () => { test('should fail to delete self', async () => {