feat: workflows & plugins (#26727)

feat: plugins

chore: better types

feat: plugins
This commit is contained in:
Jason Rasmussen
2026-05-18 11:09:33 -04:00
committed by GitHub
parent 7384799f19
commit 3d075f2bf8
144 changed files with 6099 additions and 7419 deletions
@@ -0,0 +1,56 @@
import { PluginController } from 'src/controllers/plugin.controller';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { PluginService } from 'src/services/plugin.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { factory } from 'test/small.factory';
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(PluginController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(PluginService);
beforeAll(async () => {
ctx = await controllerSetup(PluginController, [
{ provide: PluginService, useValue: service },
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});
describe('GET /plugins', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/plugins');
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.get(`/plugins`)
.query({ id: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
});
describe('GET /plugins/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/plugins/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.get(`/plugins/invalid`)
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
});
});
+22 -17
View File
@@ -1,7 +1,12 @@
import { Controller, Get, Param } from '@nestjs/common';
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto';
import {
PluginMethodResponseDto,
PluginMethodSearchDto,
PluginResponseDto,
PluginSearchDto,
} from 'src/dtos/plugin.dto';
import { Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard';
import { PluginService } from 'src/services/plugin.service';
@@ -12,26 +17,26 @@ import { UUIDParamDto } from 'src/validation';
export class PluginController {
constructor(private service: PluginService) {}
@Get('triggers')
@Authenticated({ permission: Permission.PluginRead })
@Endpoint({
summary: 'List all plugin triggers',
description: 'Retrieve a list of all available plugin triggers.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
})
getPluginTriggers(): PluginTriggerResponseDto[] {
return this.service.getTriggers();
}
@Get()
@Authenticated({ permission: Permission.PluginRead })
@Endpoint({
summary: 'List all plugins',
description: 'Retrieve a list of plugins available to the authenticated user.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
getPlugins(): Promise<PluginResponseDto[]> {
return this.service.getAll();
searchPlugins(@Query() dto: PluginSearchDto): Promise<PluginResponseDto[]> {
return this.service.search(dto);
}
@Get('methods')
@Authenticated({ permission: Permission.PluginRead })
@Endpoint({
summary: 'Retrieve plugin methods',
description: 'Retrieve a list of plugin methods',
history: HistoryBuilder.v3(),
})
searchPluginMethods(@Query() dto: PluginMethodSearchDto): Promise<PluginMethodResponseDto[]> {
return this.service.searchMethods(dto);
}
@Get(':id')
@@ -39,7 +44,7 @@ export class PluginController {
@Endpoint({
summary: 'Retrieve a plugin',
description: 'Retrieve information about a specific plugin by its ID.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
getPlugin(@Param() { id }: UUIDParamDto): Promise<PluginResponseDto> {
return this.service.get(id);
@@ -0,0 +1,113 @@
import { WorkflowController } from 'src/controllers/workflow.controller';
import { WorkflowTrigger } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { WorkflowService } from 'src/services/workflow.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { factory } from 'test/small.factory';
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(WorkflowController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(WorkflowService);
beforeAll(async () => {
ctx = await controllerSetup(WorkflowController, [
{ provide: WorkflowService, useValue: service },
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});
describe('POST /workflows', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/workflows').send({});
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require a valid trigger`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.post(`/workflows`)
.send({ trigger: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['trigger'], message: expect.stringContaining('Invalid option: expected one of') },
]),
);
});
it(`should require a valid enabled value`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.post(`/workflows`)
.send({ enabled: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['enabled'], message: 'Invalid input: expected boolean, received string' }]),
);
});
it(`should not require a name`, async () => {
const { status } = await request(ctx.getHttpServer())
.post(`/workflows`)
.send({ trigger: WorkflowTrigger.AssetCreate })
.set('Authorization', `Bearer token`);
expect(status).toBe(201);
expect(service.create).toHaveBeenCalled();
});
});
describe('GET /workflows', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/workflows');
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.get(`/workflows`)
.query({ id: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
});
describe('GET /workflows/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/workflows/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.get(`/workflows/invalid`)
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
});
describe('PUT /workflows/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/workflows/${factory.uuid()}`).send({});
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should require id to be a uuid`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.put(`/workflows/invalid`)
.set('Authorization', `Bearer token`)
.send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
});
});
+38 -9
View File
@@ -1,8 +1,15 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { WorkflowCreateDto, WorkflowResponseDto, WorkflowUpdateDto } from 'src/dtos/workflow.dto';
import {
WorkflowCreateDto,
WorkflowResponseDto,
WorkflowSearchDto,
WorkflowShareResponseDto,
WorkflowTriggerResponseDto,
WorkflowUpdateDto,
} from 'src/dtos/workflow.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { WorkflowService } from 'src/services/workflow.service';
@@ -18,7 +25,7 @@ export class WorkflowController {
@Endpoint({
summary: 'Create a workflow',
description: 'Create a new workflow, the workflow can also be created with empty filters and actions.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
createWorkflow(@Auth() auth: AuthDto, @Body() dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
return this.service.create(auth, dto);
@@ -29,10 +36,21 @@ export class WorkflowController {
@Endpoint({
summary: 'List all workflows',
description: 'Retrieve a list of workflows available to the authenticated user.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
getWorkflows(@Auth() auth: AuthDto): Promise<WorkflowResponseDto[]> {
return this.service.getAll(auth);
searchWorkflows(@Auth() auth: AuthDto, @Query() dto: WorkflowSearchDto): Promise<WorkflowResponseDto[]> {
return this.service.search(auth, dto);
}
@Get('triggers')
@Authenticated({ permission: false })
@Endpoint({
summary: 'List all workflow triggers',
description: 'Retrieve a list of all available workflow triggers.',
history: HistoryBuilder.v3(),
})
getWorkflowTriggers(): WorkflowTriggerResponseDto[] {
return this.service.getTriggers();
}
@Get(':id')
@@ -40,19 +58,30 @@ export class WorkflowController {
@Endpoint({
summary: 'Retrieve a workflow',
description: 'Retrieve information about a specific workflow by its ID.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
getWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<WorkflowResponseDto> {
return this.service.get(auth, id);
}
@Get(':id/share')
@Authenticated({ permission: Permission.WorkflowRead })
@Endpoint({
summary: 'Retrieve a workflow',
description: 'Retrieve a workflow details without ids, default values, etc.',
history: HistoryBuilder.v3(),
})
getWorkflowForShare(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<WorkflowShareResponseDto> {
return this.service.share(auth, id);
}
@Put(':id')
@Authenticated({ permission: Permission.WorkflowUpdate })
@Endpoint({
summary: 'Update a workflow',
description:
'Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
updateWorkflow(
@Auth() auth: AuthDto,
@@ -68,7 +97,7 @@ export class WorkflowController {
@Endpoint({
summary: 'Delete a workflow',
description: 'Delete a workflow by its ID.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
history: HistoryBuilder.v3(),
})
deleteWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);