diff --git a/packages/cli/src/PublicApi/types.ts b/packages/cli/src/PublicApi/types.ts index 2e80deb1af..f58b521e04 100644 --- a/packages/cli/src/PublicApi/types.ts +++ b/packages/cli/src/PublicApi/types.ts @@ -72,6 +72,7 @@ export declare namespace WorkflowRequest { workflowId?: number; active: boolean; name?: string; + projectId?: string; } >; @@ -82,6 +83,11 @@ export declare namespace WorkflowRequest { type Activate = Get; type GetTags = Get; type UpdateTags = AuthenticatedRequest<{ id: string }, {}, TagEntity[]>; + type Transfer = AuthenticatedRequest< + { workflowId: string }, + {}, + { destinationProjectId: string } + >; } export declare namespace UserRequest { diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.id.transfer.yml b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.id.transfer.yml new file mode 100644 index 0000000000..3997647e1d --- /dev/null +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.id.transfer.yml @@ -0,0 +1,31 @@ +put: + x-eov-operation-id: transferWorkflow + x-eov-operation-handler: v1/handlers/workflows/workflows.handler + tags: + - Workflow + summary: Transfer a workflow to another project. + description: Transfer a workflow to another project. + parameters: + - $ref: '../schemas/parameters/workflowId.yml' + requestBody: + description: Destination project information for the workflow transfer. + content: + application/json: + schema: + type: object + properties: + destinationProjectId: + type: string + description: The ID of the project to transfer the workflow to. + required: + - destinationProjectId + required: true + responses: + '200': + description: Operation successful. + '400': + $ref: '../../../../shared/spec/responses/badRequest.yml' + '401': + $ref: '../../../../shared/spec/responses/unauthorized.yml' + '404': + $ref: '../../../../shared/spec/responses/notFound.yml' diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.yml b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.yml index 6db149195d..1024e36cb5 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.yml +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/spec/paths/workflows.yml @@ -52,6 +52,14 @@ get: schema: type: string example: My Workflow + - name: projectId + in: query + required: false + explode: false + allowReserved: true + schema: + type: string + example: VmwOO9HeTEj20kxM - $ref: '../../../../shared/spec/parameters/limit.yml' - $ref: '../../../../shared/spec/parameters/cursor.yml' responses: diff --git a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts index 5139cb686f..ea5eebdfdb 100644 --- a/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts +++ b/packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts @@ -33,6 +33,8 @@ import { TagRepository } from '@/databases/repositories/tag.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { EventService } from '@/eventbus/event.service'; +import { z } from 'zod'; +import { EnterpriseWorkflowService } from '@/workflows/workflow.service.ee'; export = { createWorkflow: [ @@ -67,6 +69,20 @@ export = { return res.json(createdWorkflow); }, ], + transferWorkflow: [ + projectScope('workflow:move', 'workflow'), + async (req: WorkflowRequest.Transfer, res: express.Response) => { + const body = z.object({ destinationProjectId: z.string() }).parse(req.body); + + await Container.get(EnterpriseWorkflowService).transferOne( + req.user, + req.params.workflowId, + body.destinationProjectId, + ); + + res.status(204).send(); + }, + ], deleteWorkflow: [ projectScope('workflow:delete', 'workflow'), async (req: WorkflowRequest.Get, res: express.Response): Promise => { @@ -112,7 +128,7 @@ export = { getWorkflows: [ validCursor, async (req: WorkflowRequest.GetAll, res: express.Response): Promise => { - const { offset = 0, limit = 100, active, tags, name } = req.query; + const { offset = 0, limit = 100, active, tags, name, projectId } = req.query; const where: FindOptionsWhere = { ...(active !== undefined && { active }), @@ -145,6 +161,10 @@ export = { workflows = workflows.filter((wf) => workflowIds.includes(wf.id)); } + if (projectId) { + workflows = workflows.filter((w) => w.projectId === projectId); + } + if (!workflows.length) { return res.status(200).json({ data: [], diff --git a/packages/cli/src/PublicApi/v1/openapi.yml b/packages/cli/src/PublicApi/v1/openapi.yml index 91eacd5376..fd3b286a17 100644 --- a/packages/cli/src/PublicApi/v1/openapi.yml +++ b/packages/cli/src/PublicApi/v1/openapi.yml @@ -58,6 +58,8 @@ paths: $ref: './handlers/workflows/spec/paths/workflows.id.activate.yml' /workflows/{id}/deactivate: $ref: './handlers/workflows/spec/paths/workflows.id.deactivate.yml' + /workflows/{id}/transfer: + $ref: './handlers/workflows/spec/paths/workflows.id.transfer.yml' /workflows/{id}/tags: $ref: './handlers/workflows/spec/paths/workflows.id.tags.yml' /users: diff --git a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts index f8ff3523b2..4dc54935cb 100644 --- a/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts +++ b/packages/cli/src/databases/repositories/sharedWorkflow.repository.ts @@ -175,7 +175,7 @@ export class SharedWorkflowRepository extends Repository { }, }); - return sharedWorkflows.map((sw) => sw.workflow); + return sharedWorkflows.map((sw) => ({ ...sw.workflow, projectId: sw.projectId })); } /** diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 10737a30f0..855e69e3df 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -21,6 +21,8 @@ import { createTag } from '../shared/db/tags'; import { mockInstance } from '../../shared/mocking'; import type { SuperAgentTest } from '../shared/types'; import { Telemetry } from '@/telemetry'; +import { ProjectService } from '@/services/project.service'; +import { createTeamProject } from '@test-integration/db/projects'; mockInstance(Telemetry); @@ -265,6 +267,25 @@ describe('GET /workflows', () => { } }); + test('should return all user-accessible workflows filtered by `projectId`', async () => { + license.setQuota('quota:maxTeamProjects', 2); + const otherProject = await Container.get(ProjectService).createTeamProject( + 'Other project', + member, + ); + + await Promise.all([ + createWorkflow({}, member), + createWorkflow({ name: 'Other workflow' }, otherProject), + ]); + + const response = await authMemberAgent.get(`/workflows?projectId=${otherProject.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.length).toBe(1); + expect(response.body.data[0].name).toBe('Other workflow'); + }); + test('should return all owned workflows filtered by name', async () => { const workflowName = 'Workflow 1'; @@ -1465,3 +1486,44 @@ describe('PUT /workflows/:id/tags', () => { } }); }); + +describe('PUT /workflows/:id/transfer', () => { + test('should transfer workflow to project', async () => { + /** + * Arrange + */ + const firstProject = await createTeamProject('first-project', member); + const secondProject = await createTeamProject('secon-project', member); + const workflow = await createWorkflow({}, firstProject); + + /** + * Act + */ + const response = await authMemberAgent.put(`/workflows/${workflow.id}/transfer`).send({ + destinationProjectId: secondProject.id, + }); + + /** + * Assert + */ + expect(response.statusCode).toBe(204); + }); + + test('if no destination project, should reject', async () => { + /** + * Arrange + */ + const firstProject = await createTeamProject('first-project', member); + const workflow = await createWorkflow({}, firstProject); + + /** + * Act + */ + const response = await authMemberAgent.put(`/workflows/${workflow.id}/transfer`).send({}); + + /** + * Assert + */ + expect(response.statusCode).toBe(400); + }); +});