mirror of
https://github.com/immich-app/immich.git
synced 2026-05-25 00:52:31 -04:00
feat: workflows & plugins (#26727)
feat: plugins chore: better types feat: plugins
This commit is contained in:
@@ -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' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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' }]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user