mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat: more plugin triggers and methods (#28690)
This commit is contained in:
parent
58586483dc
commit
da8505f61d
28 changed files with 322 additions and 133 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { JsonSchemaDto } from 'src/dtos/json-schema.dto';
|
||||
import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum';
|
||||
import { WorkflowTriggerSchema, WorkflowType, WorkflowTypeSchema } from 'src/enum';
|
||||
import { asPluginKey } from 'src/utils/workflow';
|
||||
import z from 'zod';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { WorkflowStepConfig } from '@immich/plugin-sdk';
|
||||
import type { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { WorkflowTrigger, WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
||||
import { WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
||||
import z from 'zod';
|
||||
|
||||
const WorkflowTriggerResponseSchema = z
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import z from 'zod';
|
||||
|
||||
export enum AuthType {
|
||||
|
|
@ -1164,11 +1165,6 @@ export enum PluginContext {
|
|||
|
||||
export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' });
|
||||
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = 'AssetCreate',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
export const WorkflowTriggerSchema = z
|
||||
.enum(WorkflowTrigger)
|
||||
.describe('Plugin trigger type')
|
||||
|
|
|
|||
|
|
@ -103,6 +103,10 @@ export class WorkflowRepository {
|
|||
});
|
||||
}
|
||||
|
||||
async updateStep(id: string, dto: Updateable<WorkflowStepTable>) {
|
||||
await this.db.updateTable('workflow_step').where('workflow_step.id', '=', id).set(dto).execute();
|
||||
}
|
||||
|
||||
private async replaceAndReturn(tx: Kysely<DB>, workflowId: string, steps?: WorkflowStepUpsert[]) {
|
||||
if (steps) {
|
||||
await tx.deleteFrom('workflow_step').where('workflowId', '=', workflowId).execute();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
|
|
@ -9,7 +10,6 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from '@immich/sql-tools';
|
||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||
import { WorkflowTrigger } from 'src/enum';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
|
||||
@Table('workflow')
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { CurrentPlugin } from '@extism/extism';
|
||||
import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk';
|
||||
import {
|
||||
WorkflowChanges,
|
||||
WorkflowEventData,
|
||||
WorkflowEventPayload,
|
||||
WorkflowResponse,
|
||||
WorkflowTrigger,
|
||||
} from '@immich/plugin-sdk';
|
||||
import { HttpException, UnauthorizedException } from '@nestjs/common';
|
||||
import { join } from 'node:path';
|
||||
import { DummyValue, OnEvent, OnJob } from 'src/decorators';
|
||||
import { AlbumsAddAssetsDto } from 'src/dtos/album.dto';
|
||||
import { AlbumsAddAssetsDto, CreateAlbumDto, GetAlbumsDto } from 'src/dtos/album.dto';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
|
|
@ -15,7 +21,6 @@ import {
|
|||
JobName,
|
||||
JobStatus,
|
||||
QueueName,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
} from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
|
|
@ -61,7 +66,9 @@ export class WorkflowExecutionService extends BaseService {
|
|||
|
||||
const albumService = BaseService.create(AlbumService, this);
|
||||
|
||||
const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) =>
|
||||
const searchAlbums = this.wrap<[dto: GetAlbumsDto]>((authDto, args) => albumService.getAll(authDto, ...args));
|
||||
const createAlbum = this.wrap<[dto: CreateAlbumDto]>((authDto, args) => albumService.create(authDto, ...args));
|
||||
const addAssetsToAlbum = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) =>
|
||||
albumService.addAssets(authDto, ...args),
|
||||
);
|
||||
const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) =>
|
||||
|
|
@ -69,12 +76,16 @@ export class WorkflowExecutionService extends BaseService {
|
|||
);
|
||||
|
||||
const functions = {
|
||||
albumAddAssets,
|
||||
searchAlbums,
|
||||
createAlbum,
|
||||
addAssetsToAlbum,
|
||||
addAssetsToAlbums,
|
||||
};
|
||||
|
||||
const stubs = {
|
||||
albumAddAssets: dummy,
|
||||
const stubs: typeof functions = {
|
||||
searchAlbums: dummy,
|
||||
createAlbum: dummy,
|
||||
addAssetsToAlbum: dummy,
|
||||
addAssetsToAlbums: dummy,
|
||||
};
|
||||
|
||||
|
|
@ -252,6 +263,17 @@ export class WorkflowExecutionService extends BaseService {
|
|||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AssetMetadataExtracted' })
|
||||
onAssetMetadataExtracted({ userId, assetId, source }: ArgOf<'AssetMetadataExtracted'>) {
|
||||
// prevent loops
|
||||
// TODO loop detection in job service directly
|
||||
if (source === 'sidecar-write') {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetMetadataExtraction });
|
||||
}
|
||||
|
||||
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||
const items = await this.workflowRepository.search({ userId, trigger });
|
||||
await this.jobRepository.queueAll(
|
||||
|
|
@ -286,6 +308,25 @@ export class WorkflowExecutionService extends BaseService {
|
|||
await assetService.update(auth, assetId, {
|
||||
isFavorite: asset.isFavorite,
|
||||
visibility: asset.visibility,
|
||||
dateTimeOriginal: asset.exifInfo?.dateTimeOriginal ?? undefined,
|
||||
// TODO allow setting to null
|
||||
longitude: asset.exifInfo?.longitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
latitude: asset.exifInfo?.latitude ?? undefined,
|
||||
// TODO allow setting to null
|
||||
description: asset.exifInfo?.description ?? undefined,
|
||||
rating: asset.exifInfo?.rating,
|
||||
|
||||
// TODO add to update dto
|
||||
// make: asset.exifInfo?.make,
|
||||
// model: asset.exifInfo?.model,
|
||||
// city: asset.exifInfo?.city,
|
||||
// state: asset.exifInfo?.state,
|
||||
// country: asset.exifInfo?.country,
|
||||
// lensModel: asset.exifInfo?.lensModel,
|
||||
// fNumber: asset.exifInfo?.fNumber,
|
||||
// fps: asset.exifInfo?.fps,
|
||||
// iso: asset.exifInfo?.iso,
|
||||
});
|
||||
},
|
||||
} satisfies ExecuteOptions<typeof type>;
|
||||
|
|
@ -367,6 +408,10 @@ export class WorkflowExecutionService extends BaseService {
|
|||
({ data } = await read(type));
|
||||
}
|
||||
|
||||
if (result?.config) {
|
||||
await this.workflowRepository.updateStep(step.id, { config: result.config });
|
||||
}
|
||||
|
||||
const shouldContinue = result?.workflow?.continue ?? true;
|
||||
if (!shouldContinue) {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { WorkflowStepConfig } from '@immich/plugin-sdk';
|
||||
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
WorkflowTriggerResponseDto,
|
||||
WorkflowUpdateDto,
|
||||
} from 'src/dtos/workflow.dto';
|
||||
import { Permission, WorkflowTrigger } from 'src/enum';
|
||||
import { Permission } from 'src/enum';
|
||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { ShallowDehydrateObject } from 'kysely';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
TranscodeTarget,
|
||||
UserMetadataKey,
|
||||
VideoCodec,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
} from 'src/enum';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { WorkflowType } from 'src/enum';
|
||||
import { isMethodCompatible } from 'src/utils/workflow';
|
||||
|
||||
const tests: Array<{ trigger: WorkflowTrigger; types: WorkflowType[]; expected: boolean }> = [
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { WorkflowType } from 'src/enum';
|
||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||
|
||||
export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
||||
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
||||
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetPersonV1],
|
||||
[WorkflowTrigger.AssetMetadataExtraction]: [WorkflowType.AssetV1],
|
||||
};
|
||||
|
||||
export const getWorkflowTriggers = () =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { Kysely } from 'kysely';
|
||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { WorkflowType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { WorkflowStepConfig } from '@immich/plugin-sdk';
|
||||
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { Kysely } from 'kysely';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { AssetVisibility, LogLevel, WorkflowTrigger } from 'src/enum';
|
||||
import { AssetVisibility, LogLevel } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
|
|
@ -12,6 +12,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
|
|||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { WorkflowExecutionService } from 'src/services/workflow-execution.service';
|
||||
|
|
@ -33,8 +34,9 @@ class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
|
|||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
LoggingRepository,
|
||||
StorageRepository,
|
||||
PluginRepository,
|
||||
StorageRepository,
|
||||
UserRepository,
|
||||
WorkflowRepository,
|
||||
],
|
||||
mock: [ConfigRepository],
|
||||
|
|
@ -231,6 +233,52 @@ describe('core plugin', () => {
|
|||
});
|
||||
|
||||
describe('assetAddToAlbums', () => {
|
||||
it('should create an album by name', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [], albumName: 'Screenshots' } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
const albums = await ctx.get(AlbumRepository).getAll(user.id);
|
||||
expect(albums).toHaveLength(1);
|
||||
|
||||
const album = albums[0]!;
|
||||
expect(album.albumName).toEqual('Screenshots');
|
||||
|
||||
const updated = await ctx.get(WorkflowRepository).get(workflow.id);
|
||||
expect(updated?.steps[0].config).toEqual({ albumIds: [album.id], albumName: 'Screenshots' });
|
||||
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||
});
|
||||
|
||||
it('should not use the name when there is an albumId', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [
|
||||
{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id], albumName: 'Screenshots' } },
|
||||
],
|
||||
});
|
||||
|
||||
const albums = await ctx.get(AlbumRepository).getAll(user.id);
|
||||
expect(albums).toHaveLength(1);
|
||||
expect(albums[0].albumName).toEqual(album.albumName);
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||
});
|
||||
|
||||
it('should add an asset to an album', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue