diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index 55d02621df..bf71261278 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -5,6 +5,7 @@ import type { Scope } from '@n8n/permissions'; import type { EntityManager } from '@n8n/typeorm'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { In } from '@n8n/typeorm'; +import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import { BinaryDataService, Logger } from 'n8n-core'; @@ -27,6 +28,7 @@ import { EventService } from '@/events/event.service'; import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import { hasSharing, type ListQuery } from '@/requests'; +import { FolderService } from '@/services/folder.service'; import { OrchestrationService } from '@/services/orchestration.service'; import { OwnershipService } from '@/services/ownership.service'; import { ProjectService } from '@/services/project.service.ee'; @@ -57,6 +59,7 @@ export class WorkflowService { private readonly executionRepository: ExecutionRepository, private readonly eventService: EventService, private readonly globalConfig: GlobalConfig, + private readonly folderService: FolderService, ) {} async getMany( @@ -179,6 +182,7 @@ export class WorkflowService { workflowUpdateData: WorkflowEntity, workflowId: string, tagIds?: string[], + parentFolderId?: string, forceSave?: boolean, ): Promise { const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ @@ -263,20 +267,25 @@ export class WorkflowService { await validateEntity(workflowUpdateData); } - await this.workflowRepository.update( - workflowId, - pick(workflowUpdateData, [ - 'name', - 'active', - 'nodes', - 'connections', - 'meta', - 'settings', - 'staticData', - 'pinData', - 'versionId', - ]), - ); + const updatePayload: QueryDeepPartialEntity = pick(workflowUpdateData, [ + 'name', + 'active', + 'nodes', + 'connections', + 'meta', + 'settings', + 'staticData', + 'pinData', + 'versionId', + ]); + + if (parentFolderId) { + const project = await this.sharedWorkflowRepository.getWorkflowOwningProject(workflow.id); + await this.folderService.findFolderInProjectOrFail(parentFolderId, project?.id ?? ''); + updatePayload.parentFolder = { id: parentFolderId }; + } + + await this.workflowRepository.update(workflowId, updatePayload); const tagsDisabled = this.globalConfig.tags.disabled; diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 0744f3c5c4..538a6c253a 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -349,7 +349,7 @@ export class WorkflowsController { const forceSave = req.query.forceSave === 'true'; let updateData = new WorkflowEntity(); - const { tags, ...rest } = req.body; + const { tags, parentFolderId, ...rest } = req.body; Object.assign(updateData, rest); const isSharingEnabled = this.license.isSharingEnabled(); @@ -366,6 +366,7 @@ export class WorkflowsController { updateData, workflowId, tags, + parentFolderId, isSharingEnabled ? forceSave : true, ); diff --git a/packages/cli/test/integration/workflows/workflow.service.test.ts b/packages/cli/test/integration/workflows/workflow.service.test.ts index b7d9c033b4..6fb7422937 100644 --- a/packages/cli/test/integration/workflows/workflow.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.test.ts @@ -41,6 +41,7 @@ beforeAll(async () => { mock(), mock(), mock(), + mock(), ); }); diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 84c33415e0..f26c4c728d 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -2044,6 +2044,48 @@ describe('PATCH /workflows/:workflowId', () => { expect(updatedWorkflow.id).toBe(workflow.id); expect(updatedWorkflow.meta).toEqual(payload.meta); }); + + test('should update workflow parent folder', async () => { + const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); + const folder1 = await createFolder(ownerPersonalProject, { name: 'folder1' }); + + const workflow = await createWorkflow({}, owner); + const payload = { + versionId: workflow.versionId, + parentFolderId: folder1.id, + }; + + const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload); + + expect(response.statusCode).toBe(200); + + const updatedWorkflow = await Container.get(WorkflowRepository).findOneOrFail({ + where: { id: workflow.id }, + relations: ['parentFolder'], + }); + + expect(updatedWorkflow.parentFolder?.id).toBe(folder1.id); + }); + + test('should fail if trying update workflow parent folder with a folder that does not belong to project', async () => { + const ownerPersonalProject = await projectRepository.getPersonalProjectForUserOrFail(owner.id); + const memberPersonalProject = await projectRepository.getPersonalProjectForUserOrFail( + member.id, + ); + + await createFolder(ownerPersonalProject, { name: 'folder1' }); + const folder2 = await createFolder(memberPersonalProject, { name: 'folder2' }); + + const workflow = await createWorkflow({}, owner); + const payload = { + versionId: workflow.versionId, + parentFolderId: folder2.id, + }; + + const response = await authOwnerAgent.patch(`/workflows/${workflow.id}`).send(payload); + + expect(response.statusCode).toBe(500); + }); }); describe('POST /workflows/:workflowId/run', () => {