mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: workflow foundation (#23621)
* feat: plugins * feat: table definition * feat: type and migration * feat: add repositories * feat: validate manifest with class-validator and load manifest info to database * feat: workflow/plugin controller/service layer * feat: implement workflow logic * feat: make trigger static * feat: dynamical instantiate plugin instances * fix: access control and helper script * feat: it works * chore: simplify * refactor: refactor and use queue for workflow execution * refactor: remove unsused property in plugin-schema * build wasm in prod * feat: plugin loader in transaction * fix: docker build arm64 * generated files * shell check * fix tests * fix: waiting for migration to finish before loading plugin * remove context reassignment * feat: use mise to manage extism tools (#23760) * pr feedback * refactor: create workflow now including create filters and actions * feat: workflow medium tests * fix: broken medium test * feat: medium tests * chore: unify workflow job * sign user id with jwt * chore: query plugin with filters and action * chore: read manifest in repository * chore: load manifest from server configs * merge main * feat: endpoint documentation * pr feedback * load plugin from absolute path * refactor:handle trigger * throw error and return early * pr feedback * unify plugin services * fix: plugins code * clean up * remove triggerConfig * clean up * displayName and methodName --------- Co-authored-by: Jason Rasmussen <jason@rasm.me> Co-authored-by: bo0tzz <git@bo0tzz.me>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { DB } from 'src/schema';
|
||||
|
||||
@Injectable()
|
||||
export class PluginRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
/**
|
||||
* Loads a plugin from a validated manifest file in a transaction.
|
||||
* This ensures all plugin, filter, and action operations are atomic.
|
||||
* @param manifest The validated plugin manifest
|
||||
* @param basePath The base directory path where the plugin is located
|
||||
*/
|
||||
async loadPlugin(manifest: PluginManifestDto, basePath: string) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
// Upsert the plugin
|
||||
const plugin = await tx
|
||||
.insertInto('plugin')
|
||||
.values({
|
||||
name: manifest.name,
|
||||
title: manifest.title,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
wasmPath: `${basePath}/${manifest.wasm.path}`,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.column('name').doUpdateSet({
|
||||
title: manifest.title,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
wasmPath: `${basePath}/${manifest.wasm.path}`,
|
||||
}),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const filters = manifest.filters
|
||||
? await tx
|
||||
.insertInto('plugin_filter')
|
||||
.values(
|
||||
manifest.filters.map((filter) => ({
|
||||
pluginId: plugin.id,
|
||||
methodName: filter.methodName,
|
||||
title: filter.title,
|
||||
description: filter.description,
|
||||
supportedContexts: filter.supportedContexts,
|
||||
schema: filter.schema,
|
||||
})),
|
||||
)
|
||||
.onConflict((oc) =>
|
||||
oc.column('methodName').doUpdateSet((eb) => ({
|
||||
pluginId: eb.ref('excluded.pluginId'),
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
supportedContexts: eb.ref('excluded.supportedContexts'),
|
||||
schema: eb.ref('excluded.schema'),
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
const actions = manifest.actions
|
||||
? await tx
|
||||
.insertInto('plugin_action')
|
||||
.values(
|
||||
manifest.actions.map((action) => ({
|
||||
pluginId: plugin.id,
|
||||
methodName: action.methodName,
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
supportedContexts: action.supportedContexts,
|
||||
schema: action.schema,
|
||||
})),
|
||||
)
|
||||
.onConflict((oc) =>
|
||||
oc.column('methodName').doUpdateSet((eb) => ({
|
||||
pluginId: eb.ref('excluded.pluginId'),
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
supportedContexts: eb.ref('excluded.supportedContexts'),
|
||||
schema: eb.ref('excluded.schema'),
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
return { plugin, filters, actions };
|
||||
});
|
||||
}
|
||||
|
||||
async readDirectory(path: string) {
|
||||
return readdir(path, { withFileTypes: true });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getPlugin(id: string) {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.where('plugin.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getPluginByName(name: string) {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.where('plugin.name', '=', name)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllPlugins() {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.orderBy('plugin.name')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFilter(id: string) {
|
||||
return this.db.selectFrom('plugin_filter').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFiltersByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_filter').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAction(id: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getActionsByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user