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
+8 -10
View File
@@ -60,7 +60,8 @@ import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
import { PartnerTable } from 'src/schema/tables/partner.table';
import { PersonAuditTable } from 'src/schema/tables/person-audit.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { PluginActionTable, PluginFilterTable, PluginTable } from 'src/schema/tables/plugin.table';
import { PluginMethodTable } from 'src/schema/tables/plugin-method.table';
import { PluginTable } from 'src/schema/tables/plugin.table';
import { SessionTable } from 'src/schema/tables/session.table';
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
@@ -82,7 +83,8 @@ import {
VideoStreamSessionTable,
VideoStreamVariantTable,
} from 'src/schema/tables/video-stream.table';
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@Database({ name: 'immich' })
@@ -143,11 +145,9 @@ export class ImmichDatabase {
VideoStreamVariantTable,
VideoStreamSegmentTable,
PluginTable,
PluginFilterTable,
PluginActionTable,
PluginMethodTable,
WorkflowTable,
WorkflowFilterTable,
WorkflowActionTable,
WorkflowStepTable,
];
functions = [
@@ -264,10 +264,8 @@ export interface DB {
video_stream_segment: VideoStreamSegmentTable;
plugin: PluginTable;
plugin_filter: PluginFilterTable;
plugin_action: PluginActionTable;
plugin_method: PluginMethodTable;
workflow: WorkflowTable;
workflow_filter: WorkflowFilterTable;
workflow_action: WorkflowActionTable;
workflow_step: WorkflowStepTable;
}
@@ -0,0 +1,9 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "workflow" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "workflow" DROP COLUMN "updateId";`.execute(db);
}
@@ -0,0 +1,83 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
// take #2...
await sql`DROP TABLE "workflow_action";`.execute(db);
await sql`DROP TABLE "workflow_filter";`.execute(db);
await sql`DROP TABLE "workflow";`.execute(db);
await sql`DROP TABLE "plugin_action";`.execute(db);
await sql`DROP TABLE "plugin_filter";`.execute(db);
await sql`DROP TABLE "plugin";`.execute(db);
await sql`CREATE TABLE "plugin" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"enabled" boolean NOT NULL DEFAULT true,
"name" character varying NOT NULL,
"version" character varying NOT NULL,
"title" character varying NOT NULL,
"description" character varying NOT NULL,
"author" character varying NOT NULL,
"wasmBytes" bytea NOT NULL,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "plugin_name_version_uq" UNIQUE ("name", "version"),
CONSTRAINT "plugin_name_uq" UNIQUE ("name"),
CONSTRAINT "plugin_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "plugin_name_idx" ON "plugin" ("name");`.execute(db);
await sql`CREATE TABLE "plugin_method" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"pluginId" uuid NOT NULL,
"name" character varying NOT NULL,
"title" character varying NOT NULL,
"description" character varying NOT NULL,
"types" character varying[] NOT NULL,
"hostFunctions" boolean NOT NULL DEFAULT false,
"uiHints" character varying[] NOT NULL DEFAULT '{}',
"schema" jsonb,
CONSTRAINT "plugin_method_pluginId_fkey" FOREIGN KEY ("pluginId") REFERENCES "plugin" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "plugin_method_pluginId_name_uq" UNIQUE ("pluginId", "name"),
CONSTRAINT "plugin_method_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "plugin_method_pluginId_idx" ON "plugin_method" ("pluginId");`.execute(db);
await sql`CREATE TABLE "workflow" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"ownerId" uuid NOT NULL,
"trigger" character varying NOT NULL,
"name" character varying,
"description" character varying,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
"updateId" uuid NOT NULL DEFAULT immich_uuid_v7(),
"enabled" boolean NOT NULL DEFAULT true,
CONSTRAINT "workflow_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "workflow_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "workflow_ownerId_idx" ON "workflow" ("ownerId");`.execute(db);
await sql`CREATE OR REPLACE TRIGGER "workflow_updatedAt"
BEFORE UPDATE ON "workflow"
FOR EACH ROW
EXECUTE FUNCTION updated_at();`.execute(db);
await sql`CREATE TABLE "workflow_step" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"enabled" boolean NOT NULL DEFAULT true,
"workflowId" uuid NOT NULL,
"pluginMethodId" uuid NOT NULL,
"config" jsonb,
"order" integer NOT NULL,
CONSTRAINT "workflow_step_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflow" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "workflow_step_pluginMethodId_fkey" FOREIGN KEY ("pluginMethodId") REFERENCES "plugin_method" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "workflow_step_pkey" PRIMARY KEY ("id")
);`.execute(db);
await sql`CREATE INDEX "workflow_step_workflowId_idx" ON "workflow_step" ("workflowId");`.execute(db);
await sql`CREATE INDEX "workflow_step_pluginMethodId_idx" ON "workflow_step" ("pluginMethodId");`.execute(db);
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_workflow_updatedAt', '{"type":"trigger","name":"workflow_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"workflow_updatedAt\\"\\n BEFORE UPDATE ON \\"workflow\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(
db,
);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_plugin_filter_supportedContexts_idx';`.execute(db);
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_plugin_action_supportedContexts_idx';`.execute(db);
}
export async function down(): Promise<void> {
// not supported
}
+1 -1
View File
@@ -111,7 +111,7 @@ export class AssetExifTable {
tags!: string[] | null;
@UpdateDateColumn({ default: () => 'clock_timestamp()' })
updatedAt!: Generated<Date>;
updatedAt!: Generated<Timestamp>;
@UpdateIdColumn({ index: true })
updateId!: Generated<string>;
@@ -0,0 +1,35 @@
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table, Unique } from '@immich/sql-tools';
import { JsonSchemaDto } from 'src/dtos/json-schema.dto';
import { WorkflowType } from 'src/enum';
import { PluginTable } from 'src/schema/tables/plugin.table';
@Unique({ columns: ['pluginId', 'name'] })
@Table('plugin_method')
export class PluginMethodTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@ForeignKeyColumn(() => PluginTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
pluginId!: string;
@Column()
name!: string;
@Column()
title!: string;
@Column()
description!: string;
@Column({ type: 'character varying', array: true })
types!: Generated<WorkflowType[]>;
@Column({ type: 'boolean', default: false })
hostFunctions!: Generated<boolean>;
@Column({ type: 'jsonb', nullable: true })
schema!: JsonSchemaDto | null;
@Column({ type: 'character varying', default: [], array: true })
uiHints!: Generated<string[]>;
}
+10 -61
View File
@@ -1,25 +1,29 @@
import {
Column,
CreateDateColumn,
ForeignKeyColumn,
Generated,
Index,
PrimaryGeneratedColumn,
Table,
Timestamp,
Unique,
UpdateDateColumn,
} from '@immich/sql-tools';
import { PluginContext } from 'src/enum';
import type { JSONSchema } from 'src/types/plugin-schema.types';
@Unique({ columns: ['name', 'version'] })
@Table('plugin')
export class PluginTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@Column({ type: 'boolean', default: true })
enabled!: Generated<boolean>;
@Column({ index: true, unique: true })
name!: string;
@Column()
version!: string;
@Column()
title!: string;
@@ -29,11 +33,8 @@ export class PluginTable {
@Column()
author!: string;
@Column()
version!: string;
@Column()
wasmPath!: string;
@Column({ type: 'bytea' })
wasmBytes!: Buffer;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@@ -41,55 +42,3 @@ export class PluginTable {
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
}
@Index({ columns: ['supportedContexts'], using: 'gin' })
@Table('plugin_filter')
export class PluginFilterTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@ForeignKeyColumn(() => PluginTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@Column({ index: true })
pluginId!: string;
@Column({ index: true, unique: true })
methodName!: string;
@Column()
title!: string;
@Column()
description!: string;
@Column({ type: 'character varying', array: true })
supportedContexts!: Generated<PluginContext[]>;
@Column({ type: 'jsonb', nullable: true })
schema!: JSONSchema | null;
}
@Index({ columns: ['supportedContexts'], using: 'gin' })
@Table('plugin_action')
export class PluginActionTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@ForeignKeyColumn(() => PluginTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@Column({ index: true })
pluginId!: string;
@Column({ index: true, unique: true })
methodName!: string;
@Column()
title!: string;
@Column()
description!: string;
@Column({ type: 'character varying', array: true })
supportedContexts!: Generated<PluginContext[]>;
@Column({ type: 'jsonb', nullable: true })
schema!: JSONSchema | null;
}
@@ -0,0 +1,26 @@
import { WorkflowStepConfig } from '@immich/plugin-sdk';
import { Column, ForeignKeyColumn, PrimaryGeneratedColumn, Table } from '@immich/sql-tools';
import { Generated } from 'kysely';
import { PluginMethodTable } from 'src/schema/tables/plugin-method.table';
import { WorkflowTable } from 'src/schema/tables/workflow.table';
@Table('workflow_step')
export class WorkflowStepTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@Column({ type: 'boolean', default: true })
enabled!: boolean;
@ForeignKeyColumn(() => WorkflowTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
workflowId!: string;
@ForeignKeyColumn(() => PluginMethodTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
pluginMethodId!: string;
@Column({ type: 'jsonb', nullable: true })
config!: WorkflowStepConfig | null;
@Column({ type: 'integer' })
order!: number;
}
+14 -48
View File
@@ -3,17 +3,17 @@ import {
CreateDateColumn,
ForeignKeyColumn,
Generated,
Index,
PrimaryGeneratedColumn,
Table,
Timestamp,
UpdateDateColumn,
} from '@immich/sql-tools';
import { PluginTriggerType } from 'src/enum';
import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { WorkflowTrigger } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types';
@Table('workflow')
@UpdatedAtTrigger('workflow_updatedAt')
export class WorkflowTable {
@PrimaryGeneratedColumn()
id!: Generated<string>;
@@ -22,57 +22,23 @@ export class WorkflowTable {
ownerId!: string;
@Column()
triggerType!: PluginTriggerType;
trigger!: WorkflowTrigger;
@Column({ nullable: true })
name!: string | null;
@Column()
description!: string;
@Column({ nullable: true })
description!: string | null;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
@UpdateIdColumn()
updateId!: Generated<string>;
@Column({ type: 'boolean', default: true })
enabled!: boolean;
}
@Index({ columns: ['workflowId', 'order'] })
@Index({ columns: ['pluginFilterId'] })
@Table('workflow_filter')
export class WorkflowFilterTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@ForeignKeyColumn(() => WorkflowTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
workflowId!: Generated<string>;
@ForeignKeyColumn(() => PluginFilterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
pluginFilterId!: string;
@Column({ type: 'jsonb', nullable: true })
filterConfig!: FilterConfig | null;
@Column({ type: 'integer' })
order!: number;
}
@Index({ columns: ['workflowId', 'order'] })
@Index({ columns: ['pluginActionId'] })
@Table('workflow_action')
export class WorkflowActionTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@ForeignKeyColumn(() => WorkflowTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
workflowId!: Generated<string>;
@ForeignKeyColumn(() => PluginActionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
pluginActionId!: string;
@Column({ type: 'jsonb', nullable: true })
actionConfig!: ActionConfig | null;
@Column({ type: 'integer' })
order!: number;
enabled!: Generated<boolean>;
}