feat(core): Update endpoint to update a workflow, to support updating the workflow parent folder (no-chagelog) (#13906)

This commit is contained in:
Ricardo Espinoza
2025-03-17 12:06:52 -04:00
committed by GitHub
parent d0fdb11499
commit 3a5cc4ae95
4 changed files with 68 additions and 15 deletions

View File

@@ -5,6 +5,7 @@ import type { Scope } from '@n8n/permissions';
import type { EntityManager } from '@n8n/typeorm'; import type { EntityManager } from '@n8n/typeorm';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm'; import { In } from '@n8n/typeorm';
import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { BinaryDataService, Logger } from 'n8n-core'; import { BinaryDataService, Logger } from 'n8n-core';
@@ -27,6 +28,7 @@ import { EventService } from '@/events/event.service';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';
import { validateEntity } from '@/generic-helpers'; import { validateEntity } from '@/generic-helpers';
import { hasSharing, type ListQuery } from '@/requests'; import { hasSharing, type ListQuery } from '@/requests';
import { FolderService } from '@/services/folder.service';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { ProjectService } from '@/services/project.service.ee'; import { ProjectService } from '@/services/project.service.ee';
@@ -57,6 +59,7 @@ export class WorkflowService {
private readonly executionRepository: ExecutionRepository, private readonly executionRepository: ExecutionRepository,
private readonly eventService: EventService, private readonly eventService: EventService,
private readonly globalConfig: GlobalConfig, private readonly globalConfig: GlobalConfig,
private readonly folderService: FolderService,
) {} ) {}
async getMany( async getMany(
@@ -179,6 +182,7 @@ export class WorkflowService {
workflowUpdateData: WorkflowEntity, workflowUpdateData: WorkflowEntity,
workflowId: string, workflowId: string,
tagIds?: string[], tagIds?: string[],
parentFolderId?: string,
forceSave?: boolean, forceSave?: boolean,
): Promise<WorkflowEntity> { ): Promise<WorkflowEntity> {
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [ const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
@@ -263,20 +267,25 @@ export class WorkflowService {
await validateEntity(workflowUpdateData); await validateEntity(workflowUpdateData);
} }
await this.workflowRepository.update( const updatePayload: QueryDeepPartialEntity<WorkflowEntity> = pick(workflowUpdateData, [
workflowId, 'name',
pick(workflowUpdateData, [ 'active',
'name', 'nodes',
'active', 'connections',
'nodes', 'meta',
'connections', 'settings',
'meta', 'staticData',
'settings', 'pinData',
'staticData', 'versionId',
'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; const tagsDisabled = this.globalConfig.tags.disabled;

View File

@@ -349,7 +349,7 @@ export class WorkflowsController {
const forceSave = req.query.forceSave === 'true'; const forceSave = req.query.forceSave === 'true';
let updateData = new WorkflowEntity(); let updateData = new WorkflowEntity();
const { tags, ...rest } = req.body; const { tags, parentFolderId, ...rest } = req.body;
Object.assign(updateData, rest); Object.assign(updateData, rest);
const isSharingEnabled = this.license.isSharingEnabled(); const isSharingEnabled = this.license.isSharingEnabled();
@@ -366,6 +366,7 @@ export class WorkflowsController {
updateData, updateData,
workflowId, workflowId,
tags, tags,
parentFolderId,
isSharingEnabled ? forceSave : true, isSharingEnabled ? forceSave : true,
); );

View File

@@ -41,6 +41,7 @@ beforeAll(async () => {
mock(), mock(),
mock(), mock(),
mock(), mock(),
mock(),
); );
}); });

View File

@@ -2044,6 +2044,48 @@ describe('PATCH /workflows/:workflowId', () => {
expect(updatedWorkflow.id).toBe(workflow.id); expect(updatedWorkflow.id).toBe(workflow.id);
expect(updatedWorkflow.meta).toEqual(payload.meta); 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', () => { describe('POST /workflows/:workflowId/run', () => {