mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat(core): Allow filtering workflows by project and transferring workflows in Public API (#10231)
This commit is contained in:
parent
452f52c124
commit
d719899223
|
@ -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 {
|
||||
|
|
|
@ -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'
|
|
@ -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:
|
||||
|
|
|
@ -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<express.Response> => {
|
||||
|
@ -112,7 +128,7 @@ export = {
|
|||
getWorkflows: [
|
||||
validCursor,
|
||||
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
|
||||
const { offset = 0, limit = 100, active, tags, name } = req.query;
|
||||
const { offset = 0, limit = 100, active, tags, name, projectId } = req.query;
|
||||
|
||||
const where: FindOptionsWhere<WorkflowEntity> = {
|
||||
...(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: [],
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -175,7 +175,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
|
|||
},
|
||||
});
|
||||
|
||||
return sharedWorkflows.map((sw) => sw.workflow);
|
||||
return sharedWorkflows.map((sw) => ({ ...sw.workflow, projectId: sw.projectId }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue