mirror of
https://github.com/immich-app/immich.git
synced 2026-03-13 05:46:54 -07:00
Compare commits
1 Commits
fix/album-
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e04007f8b |
@@ -16,7 +16,7 @@ services:
|
||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- pnpm_store_server:/buildcache/pnpm-store
|
||||
- ../plugins:/build/corePlugin
|
||||
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||
immich-web:
|
||||
env_file: !reset []
|
||||
immich-machine-learning:
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -90,6 +90,8 @@ jobs:
|
||||
- name: Run formatter
|
||||
run: pnpm format
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Build app
|
||||
run: pnpm run --filter immich --filter @immich/plugin-sdk build
|
||||
- name: Run tsc
|
||||
run: pnpm check
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -394,6 +396,8 @@ jobs:
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Run pnpm install
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||
- name: Build plugin
|
||||
run: pnpm run --filter @immich/plugin-sdk --filter @immich/plugin-core build
|
||||
- name: Run medium tests
|
||||
run: pnpm test:medium
|
||||
if: ${{ !cancelled() }}
|
||||
@@ -722,7 +726,7 @@ jobs:
|
||||
- name: Install server dependencies
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
|
||||
- name: Build the app
|
||||
run: pnpm --filter immich build
|
||||
run: pnpm --filter immich --filter @immich/plugin-sdk build
|
||||
- name: Run API generation
|
||||
run: ./bin/generate-open-api.sh
|
||||
working-directory: open-api
|
||||
@@ -784,7 +788,7 @@ jobs:
|
||||
- name: Install server dependencies
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||
- name: Build the app
|
||||
run: pnpm build
|
||||
run: pnpm run --filter immich --filter @immich/plugin-sdk build
|
||||
- name: Run existing migrations
|
||||
run: pnpm migrations:run
|
||||
- name: Test npm run schema:reset command works
|
||||
|
||||
@@ -73,7 +73,7 @@ services:
|
||||
- ${UPLOAD_LOCATION}/photos:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- pnpm_store_server:/buildcache/pnpm-store
|
||||
- ../plugins:/build/corePlugin
|
||||
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
11
i18n/en.json
11
i18n/en.json
@@ -22,13 +22,12 @@
|
||||
"add_birthday": "Add a birthday",
|
||||
"add_endpoint": "Add endpoint",
|
||||
"add_exclusion_pattern": "Add exclusion pattern",
|
||||
"add_filter": "Add filter",
|
||||
"add_filter_description": "Click to add a filter condition",
|
||||
"add_location": "Add location",
|
||||
"add_more_users": "Add more users",
|
||||
"add_partner": "Add partner",
|
||||
"add_path": "Add path",
|
||||
"add_photos": "Add photos",
|
||||
"add_step": "Add step",
|
||||
"add_tag": "Add tag",
|
||||
"add_to": "Add to…",
|
||||
"add_to_album": "Add to album",
|
||||
@@ -42,7 +41,6 @@
|
||||
"add_to_shared_album": "Add to shared album",
|
||||
"add_upload_to_stack": "Add upload to stack",
|
||||
"add_url": "Add URL",
|
||||
"add_workflow_step": "Add workflow step",
|
||||
"added_to_archive": "Added to archive",
|
||||
"added_to_favorites": "Added to favorites",
|
||||
"added_to_favorites_count": "Added {count, number} to favorites",
|
||||
@@ -805,6 +803,7 @@
|
||||
"comments_are_disabled": "Comments are disabled",
|
||||
"common_create_new_album": "Create new album",
|
||||
"completed": "Completed",
|
||||
"configuration": "Configuration",
|
||||
"confirm": "Confirm",
|
||||
"confirm_admin_password": "Confirm Admin Password",
|
||||
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
||||
@@ -1583,7 +1582,6 @@
|
||||
"next": "Next",
|
||||
"next_memory": "Next memory",
|
||||
"no": "No",
|
||||
"no_actions_added": "No actions added yet",
|
||||
"no_albums_found": "No albums found",
|
||||
"no_albums_message": "Create an album to organize your photos and videos",
|
||||
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
|
||||
@@ -1600,7 +1598,6 @@
|
||||
"no_exif_info_available": "No exif info available",
|
||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
||||
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
||||
"no_filters_added": "No filters added yet",
|
||||
"no_libraries_message": "Create an external library to view your photos and videos",
|
||||
"no_local_assets_found": "No local assets found with this checksum",
|
||||
"no_location_set": "No location set",
|
||||
@@ -1613,6 +1610,7 @@
|
||||
"no_results": "No results",
|
||||
"no_results_description": "Try a synonym or more general keyword",
|
||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||
"no_steps": "No steps added yet",
|
||||
"no_uploads_in_progress": "No uploads in progress",
|
||||
"none": "None",
|
||||
"not_allowed": "Not allowed",
|
||||
@@ -2181,6 +2179,7 @@
|
||||
"start_date_before_end_date": "Start date must be before end date",
|
||||
"state": "State",
|
||||
"status": "Status",
|
||||
"steps": "Steps",
|
||||
"stop_casting": "Stop casting",
|
||||
"stop_motion_photo": "Stop Motion Photo",
|
||||
"stop_photo_sharing": "Stop sharing your photos?",
|
||||
@@ -2311,7 +2310,6 @@
|
||||
"unsupported_field_type": "Unsupported field type",
|
||||
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
|
||||
"untagged": "Untagged",
|
||||
"untitled_workflow": "Untitled workflow",
|
||||
"up_next": "Up next",
|
||||
"update_location_action_prompt": "Update the location of {count} selected assets with:",
|
||||
"updated_at": "Updated",
|
||||
@@ -2402,6 +2400,7 @@
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"width": "Width",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"workflow": "Workflow",
|
||||
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
|
||||
"workflow_deleted": "Workflow deleted",
|
||||
"workflow_description": "Workflow description",
|
||||
|
||||
@@ -2,7 +2,7 @@ experimental_monorepo_root = true
|
||||
|
||||
[monorepo]
|
||||
config_roots = [
|
||||
"plugins",
|
||||
"packages/plugin-core",
|
||||
"server",
|
||||
"cli",
|
||||
"deployment",
|
||||
|
||||
22
mobile/openapi/README.md
generated
22
mobile/openapi/README.md
generated
@@ -211,8 +211,8 @@ Class | Method | HTTP request | Description
|
||||
*PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people
|
||||
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
|
||||
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
|
||||
*PluginsApi* | [**getPluginTriggers**](doc//PluginsApi.md#getplugintriggers) | **GET** /plugins/triggers | List all plugin triggers
|
||||
*PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins
|
||||
*PluginsApi* | [**searchPluginMethods**](doc//PluginsApi.md#searchpluginmethods) | **GET** /plugins/methods | Retrieve plugin methods
|
||||
*PluginsApi* | [**searchPlugins**](doc//PluginsApi.md#searchplugins) | **GET** /plugins | List all plugins
|
||||
*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue
|
||||
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
|
||||
*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs
|
||||
@@ -323,7 +323,8 @@ Class | Method | HTTP request | Description
|
||||
*WorkflowsApi* | [**createWorkflow**](doc//WorkflowsApi.md#createworkflow) | **POST** /workflows | Create a workflow
|
||||
*WorkflowsApi* | [**deleteWorkflow**](doc//WorkflowsApi.md#deleteworkflow) | **DELETE** /workflows/{id} | Delete a workflow
|
||||
*WorkflowsApi* | [**getWorkflow**](doc//WorkflowsApi.md#getworkflow) | **GET** /workflows/{id} | Retrieve a workflow
|
||||
*WorkflowsApi* | [**getWorkflows**](doc//WorkflowsApi.md#getworkflows) | **GET** /workflows | List all workflows
|
||||
*WorkflowsApi* | [**getWorkflowTriggers**](doc//WorkflowsApi.md#getworkflowtriggers) | **GET** /workflows/triggers | List all workflow triggers
|
||||
*WorkflowsApi* | [**searchWorkflows**](doc//WorkflowsApi.md#searchworkflows) | **GET** /workflows | List all workflows
|
||||
*WorkflowsApi* | [**updateWorkflow**](doc//WorkflowsApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
|
||||
|
||||
|
||||
@@ -498,12 +499,8 @@ Class | Method | HTTP request | Description
|
||||
- [PinCodeResetDto](doc//PinCodeResetDto.md)
|
||||
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||
- [PluginActionResponseDto](doc//PluginActionResponseDto.md)
|
||||
- [PluginContextType](doc//PluginContextType.md)
|
||||
- [PluginFilterResponseDto](doc//PluginFilterResponseDto.md)
|
||||
- [PluginMethodResponseDto](doc//PluginMethodResponseDto.md)
|
||||
- [PluginResponseDto](doc//PluginResponseDto.md)
|
||||
- [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md)
|
||||
- [PluginTriggerType](doc//PluginTriggerType.md)
|
||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||
- [QueueCommand](doc//QueueCommand.md)
|
||||
@@ -675,12 +672,13 @@ Class | Method | HTTP request | Description
|
||||
- [VersionCheckStateResponseDto](doc//VersionCheckStateResponseDto.md)
|
||||
- [VideoCodec](doc//VideoCodec.md)
|
||||
- [VideoContainer](doc//VideoContainer.md)
|
||||
- [WorkflowActionItemDto](doc//WorkflowActionItemDto.md)
|
||||
- [WorkflowActionResponseDto](doc//WorkflowActionResponseDto.md)
|
||||
- [WorkflowCreateDto](doc//WorkflowCreateDto.md)
|
||||
- [WorkflowFilterItemDto](doc//WorkflowFilterItemDto.md)
|
||||
- [WorkflowFilterResponseDto](doc//WorkflowFilterResponseDto.md)
|
||||
- [WorkflowResponseDto](doc//WorkflowResponseDto.md)
|
||||
- [WorkflowStepDto](doc//WorkflowStepDto.md)
|
||||
- [WorkflowStepResponseDto](doc//WorkflowStepResponseDto.md)
|
||||
- [WorkflowTrigger](doc//WorkflowTrigger.md)
|
||||
- [WorkflowTriggerResponseDto](doc//WorkflowTriggerResponseDto.md)
|
||||
- [WorkflowType](doc//WorkflowType.md)
|
||||
- [WorkflowUpdateDto](doc//WorkflowUpdateDto.md)
|
||||
|
||||
|
||||
|
||||
15
mobile/openapi/lib/api.dart
generated
15
mobile/openapi/lib/api.dart
generated
@@ -237,12 +237,8 @@ part 'model/pin_code_change_dto.dart';
|
||||
part 'model/pin_code_reset_dto.dart';
|
||||
part 'model/pin_code_setup_dto.dart';
|
||||
part 'model/places_response_dto.dart';
|
||||
part 'model/plugin_action_response_dto.dart';
|
||||
part 'model/plugin_context_type.dart';
|
||||
part 'model/plugin_filter_response_dto.dart';
|
||||
part 'model/plugin_method_response_dto.dart';
|
||||
part 'model/plugin_response_dto.dart';
|
||||
part 'model/plugin_trigger_response_dto.dart';
|
||||
part 'model/plugin_trigger_type.dart';
|
||||
part 'model/purchase_response.dart';
|
||||
part 'model/purchase_update.dart';
|
||||
part 'model/queue_command.dart';
|
||||
@@ -414,12 +410,13 @@ part 'model/validate_library_response_dto.dart';
|
||||
part 'model/version_check_state_response_dto.dart';
|
||||
part 'model/video_codec.dart';
|
||||
part 'model/video_container.dart';
|
||||
part 'model/workflow_action_item_dto.dart';
|
||||
part 'model/workflow_action_response_dto.dart';
|
||||
part 'model/workflow_create_dto.dart';
|
||||
part 'model/workflow_filter_item_dto.dart';
|
||||
part 'model/workflow_filter_response_dto.dart';
|
||||
part 'model/workflow_response_dto.dart';
|
||||
part 'model/workflow_step_dto.dart';
|
||||
part 'model/workflow_step_response_dto.dart';
|
||||
part 'model/workflow_trigger.dart';
|
||||
part 'model/workflow_trigger_response_dto.dart';
|
||||
part 'model/workflow_type.dart';
|
||||
part 'model/workflow_update_dto.dart';
|
||||
|
||||
|
||||
|
||||
149
mobile/openapi/lib/api/plugins_api.dart
generated
149
mobile/openapi/lib/api/plugins_api.dart
generated
@@ -73,14 +73,36 @@ class PluginsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all plugin triggers
|
||||
/// Retrieve plugin methods
|
||||
///
|
||||
/// Retrieve a list of all available plugin triggers.
|
||||
/// Retrieve a list of plugin methods
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getPluginTriggersWithHttpInfo() async {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Whether the plugin method is enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Plugin method ID
|
||||
///
|
||||
/// * [String] name:
|
||||
///
|
||||
/// * [String] pluginName:
|
||||
///
|
||||
/// * [String] pluginVersion:
|
||||
///
|
||||
/// * [String] title:
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
///
|
||||
/// * [WorkflowType] type:
|
||||
Future<Response> searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/plugins/triggers';
|
||||
final apiPath = r'/plugins/methods';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
@@ -89,6 +111,34 @@ class PluginsApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (description != null) {
|
||||
queryParams.addAll(_queryParams('', 'description', description));
|
||||
}
|
||||
if (enabled != null) {
|
||||
queryParams.addAll(_queryParams('', 'enabled', enabled));
|
||||
}
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (name != null) {
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
}
|
||||
if (pluginName != null) {
|
||||
queryParams.addAll(_queryParams('', 'pluginName', pluginName));
|
||||
}
|
||||
if (pluginVersion != null) {
|
||||
queryParams.addAll(_queryParams('', 'pluginVersion', pluginVersion));
|
||||
}
|
||||
if (title != null) {
|
||||
queryParams.addAll(_queryParams('', 'title', title));
|
||||
}
|
||||
if (trigger != null) {
|
||||
queryParams.addAll(_queryParams('', 'trigger', trigger));
|
||||
}
|
||||
if (type != null) {
|
||||
queryParams.addAll(_queryParams('', 'type', type));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -103,11 +153,33 @@ class PluginsApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// List all plugin triggers
|
||||
/// Retrieve plugin methods
|
||||
///
|
||||
/// Retrieve a list of all available plugin triggers.
|
||||
Future<List<PluginTriggerResponseDto>?> getPluginTriggers() async {
|
||||
final response = await getPluginTriggersWithHttpInfo();
|
||||
/// Retrieve a list of plugin methods
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Whether the plugin method is enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Plugin method ID
|
||||
///
|
||||
/// * [String] name:
|
||||
///
|
||||
/// * [String] pluginName:
|
||||
///
|
||||
/// * [String] pluginVersion:
|
||||
///
|
||||
/// * [String] title:
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
///
|
||||
/// * [WorkflowType] type:
|
||||
Future<List<PluginMethodResponseDto>?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async {
|
||||
final response = await searchPluginMethodsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -116,8 +188,8 @@ class PluginsApi {
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<PluginTriggerResponseDto>') as List)
|
||||
.cast<PluginTriggerResponseDto>()
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<PluginMethodResponseDto>') as List)
|
||||
.cast<PluginMethodResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
@@ -129,7 +201,23 @@ class PluginsApi {
|
||||
/// Retrieve a list of plugins available to the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getPluginsWithHttpInfo() async {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Whether the plugin is enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Plugin ID
|
||||
///
|
||||
/// * [String] name:
|
||||
///
|
||||
/// * [String] title:
|
||||
///
|
||||
/// * [String] version:
|
||||
Future<Response> searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/plugins';
|
||||
|
||||
@@ -140,6 +228,25 @@ class PluginsApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (description != null) {
|
||||
queryParams.addAll(_queryParams('', 'description', description));
|
||||
}
|
||||
if (enabled != null) {
|
||||
queryParams.addAll(_queryParams('', 'enabled', enabled));
|
||||
}
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (name != null) {
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
}
|
||||
if (title != null) {
|
||||
queryParams.addAll(_queryParams('', 'title', title));
|
||||
}
|
||||
if (version != null) {
|
||||
queryParams.addAll(_queryParams('', 'version', version));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -157,8 +264,24 @@ class PluginsApi {
|
||||
/// List all plugins
|
||||
///
|
||||
/// Retrieve a list of plugins available to the authenticated user.
|
||||
Future<List<PluginResponseDto>?> getPlugins() async {
|
||||
final response = await getPluginsWithHttpInfo();
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Whether the plugin is enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Plugin ID
|
||||
///
|
||||
/// * [String] name:
|
||||
///
|
||||
/// * [String] title:
|
||||
///
|
||||
/// * [String] version:
|
||||
Future<List<PluginResponseDto>?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async {
|
||||
final response = await searchPluginsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, title: title, version: version, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
113
mobile/openapi/lib/api/workflows_api.dart
generated
113
mobile/openapi/lib/api/workflows_api.dart
generated
@@ -178,14 +178,14 @@ class WorkflowsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all workflows
|
||||
/// List all workflow triggers
|
||||
///
|
||||
/// Retrieve a list of workflows available to the authenticated user.
|
||||
/// Retrieve a list of all available workflow triggers.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getWorkflowsWithHttpInfo() async {
|
||||
Future<Response> getWorkflowTriggersWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows';
|
||||
final apiPath = r'/workflows/triggers';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
@@ -208,11 +208,112 @@ class WorkflowsApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// List all workflow triggers
|
||||
///
|
||||
/// Retrieve a list of all available workflow triggers.
|
||||
Future<List<WorkflowTriggerResponseDto>?> getWorkflowTriggers() async {
|
||||
final response = await getWorkflowTriggersWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<WorkflowTriggerResponseDto>') as List)
|
||||
.cast<WorkflowTriggerResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all workflows
|
||||
///
|
||||
/// Retrieve a list of workflows available to the authenticated user.
|
||||
Future<List<WorkflowResponseDto>?> getWorkflows() async {
|
||||
final response = await getWorkflowsWithHttpInfo();
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
/// Workflow description
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Workflow enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Workflow ID
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Workflow name
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
/// Workflow trigger type
|
||||
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (description != null) {
|
||||
queryParams.addAll(_queryParams('', 'description', description));
|
||||
}
|
||||
if (enabled != null) {
|
||||
queryParams.addAll(_queryParams('', 'enabled', enabled));
|
||||
}
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (name != null) {
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
}
|
||||
if (trigger != null) {
|
||||
queryParams.addAll(_queryParams('', 'trigger', trigger));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// List all workflows
|
||||
///
|
||||
/// Retrieve a list of workflows available to the authenticated user.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] description:
|
||||
/// Workflow description
|
||||
///
|
||||
/// * [bool] enabled:
|
||||
/// Workflow enabled
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Workflow ID
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Workflow name
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
/// Workflow trigger type
|
||||
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async {
|
||||
final response = await searchWorkflowsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, trigger: trigger, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
30
mobile/openapi/lib/api_client.dart
generated
30
mobile/openapi/lib/api_client.dart
generated
@@ -520,18 +520,10 @@ class ApiClient {
|
||||
return PinCodeSetupDto.fromJson(value);
|
||||
case 'PlacesResponseDto':
|
||||
return PlacesResponseDto.fromJson(value);
|
||||
case 'PluginActionResponseDto':
|
||||
return PluginActionResponseDto.fromJson(value);
|
||||
case 'PluginContextType':
|
||||
return PluginContextTypeTypeTransformer().decode(value);
|
||||
case 'PluginFilterResponseDto':
|
||||
return PluginFilterResponseDto.fromJson(value);
|
||||
case 'PluginMethodResponseDto':
|
||||
return PluginMethodResponseDto.fromJson(value);
|
||||
case 'PluginResponseDto':
|
||||
return PluginResponseDto.fromJson(value);
|
||||
case 'PluginTriggerResponseDto':
|
||||
return PluginTriggerResponseDto.fromJson(value);
|
||||
case 'PluginTriggerType':
|
||||
return PluginTriggerTypeTypeTransformer().decode(value);
|
||||
case 'PurchaseResponse':
|
||||
return PurchaseResponse.fromJson(value);
|
||||
case 'PurchaseUpdate':
|
||||
@@ -874,18 +866,20 @@ class ApiClient {
|
||||
return VideoCodecTypeTransformer().decode(value);
|
||||
case 'VideoContainer':
|
||||
return VideoContainerTypeTransformer().decode(value);
|
||||
case 'WorkflowActionItemDto':
|
||||
return WorkflowActionItemDto.fromJson(value);
|
||||
case 'WorkflowActionResponseDto':
|
||||
return WorkflowActionResponseDto.fromJson(value);
|
||||
case 'WorkflowCreateDto':
|
||||
return WorkflowCreateDto.fromJson(value);
|
||||
case 'WorkflowFilterItemDto':
|
||||
return WorkflowFilterItemDto.fromJson(value);
|
||||
case 'WorkflowFilterResponseDto':
|
||||
return WorkflowFilterResponseDto.fromJson(value);
|
||||
case 'WorkflowResponseDto':
|
||||
return WorkflowResponseDto.fromJson(value);
|
||||
case 'WorkflowStepDto':
|
||||
return WorkflowStepDto.fromJson(value);
|
||||
case 'WorkflowStepResponseDto':
|
||||
return WorkflowStepResponseDto.fromJson(value);
|
||||
case 'WorkflowTrigger':
|
||||
return WorkflowTriggerTypeTransformer().decode(value);
|
||||
case 'WorkflowTriggerResponseDto':
|
||||
return WorkflowTriggerResponseDto.fromJson(value);
|
||||
case 'WorkflowType':
|
||||
return WorkflowTypeTypeTransformer().decode(value);
|
||||
case 'WorkflowUpdateDto':
|
||||
return WorkflowUpdateDto.fromJson(value);
|
||||
default:
|
||||
|
||||
12
mobile/openapi/lib/api_helper.dart
generated
12
mobile/openapi/lib/api_helper.dart
generated
@@ -130,12 +130,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is Permission) {
|
||||
return PermissionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PluginContextType) {
|
||||
return PluginContextTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PluginTriggerType) {
|
||||
return PluginTriggerTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueCommand) {
|
||||
return QueueCommandTypeTransformer().encode(value).toString();
|
||||
}
|
||||
@@ -193,6 +187,12 @@ String parameterToString(dynamic value) {
|
||||
if (value is VideoContainer) {
|
||||
return VideoContainerTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is WorkflowTrigger) {
|
||||
return WorkflowTriggerTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is WorkflowType) {
|
||||
return WorkflowTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
|
||||
6
mobile/openapi/lib/model/job_name.dart
generated
6
mobile/openapi/lib/model/job_name.dart
generated
@@ -78,7 +78,7 @@ class JobName {
|
||||
static const versionCheck = JobName._(r'VersionCheck');
|
||||
static const ocrQueueAll = JobName._(r'OcrQueueAll');
|
||||
static const ocr = JobName._(r'Ocr');
|
||||
static const workflowRun = JobName._(r'WorkflowRun');
|
||||
static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate');
|
||||
|
||||
/// List of all possible values in this [enum][JobName].
|
||||
static const values = <JobName>[
|
||||
@@ -137,7 +137,7 @@ class JobName {
|
||||
versionCheck,
|
||||
ocrQueueAll,
|
||||
ocr,
|
||||
workflowRun,
|
||||
workflowAssetCreate,
|
||||
];
|
||||
|
||||
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
|
||||
@@ -231,7 +231,7 @@ class JobNameTypeTransformer {
|
||||
case r'VersionCheck': return JobName.versionCheck;
|
||||
case r'OcrQueueAll': return JobName.ocrQueueAll;
|
||||
case r'Ocr': return JobName.ocr;
|
||||
case r'WorkflowRun': return JobName.workflowRun;
|
||||
case r'WorkflowAssetCreate': return JobName.workflowAssetCreate;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
88
mobile/openapi/lib/model/plugin_context_type.dart
generated
88
mobile/openapi/lib/model/plugin_context_type.dart
generated
@@ -1,88 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Context type
|
||||
class PluginContextType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const PluginContextType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const asset = PluginContextType._(r'asset');
|
||||
static const album = PluginContextType._(r'album');
|
||||
static const person = PluginContextType._(r'person');
|
||||
|
||||
/// List of all possible values in this [enum][PluginContextType].
|
||||
static const values = <PluginContextType>[
|
||||
asset,
|
||||
album,
|
||||
person,
|
||||
];
|
||||
|
||||
static PluginContextType? fromJson(dynamic value) => PluginContextTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<PluginContextType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginContextType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginContextType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [PluginContextType] to String,
|
||||
/// and [decode] dynamic data back to [PluginContextType].
|
||||
class PluginContextTypeTypeTransformer {
|
||||
factory PluginContextTypeTypeTransformer() => _instance ??= const PluginContextTypeTypeTransformer._();
|
||||
|
||||
const PluginContextTypeTypeTransformer._();
|
||||
|
||||
String encode(PluginContextType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a PluginContextType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
PluginContextType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'asset': return PluginContextType.asset;
|
||||
case r'album': return PluginContextType.album;
|
||||
case r'person': return PluginContextType.person;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [PluginContextTypeTypeTransformer] instance.
|
||||
static PluginContextTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
158
mobile/openapi/lib/model/plugin_filter_response_dto.dart
generated
158
mobile/openapi/lib/model/plugin_filter_response_dto.dart
generated
@@ -1,158 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PluginFilterResponseDto {
|
||||
/// Returns a new [PluginFilterResponseDto] instance.
|
||||
PluginFilterResponseDto({
|
||||
required this.description,
|
||||
required this.id,
|
||||
required this.methodName,
|
||||
required this.pluginId,
|
||||
required this.schema,
|
||||
this.supportedContexts = const [],
|
||||
required this.title,
|
||||
});
|
||||
|
||||
/// Filter description
|
||||
String description;
|
||||
|
||||
/// Filter ID
|
||||
String id;
|
||||
|
||||
/// Method name
|
||||
String methodName;
|
||||
|
||||
/// Plugin ID
|
||||
String pluginId;
|
||||
|
||||
/// Filter schema
|
||||
Object? schema;
|
||||
|
||||
/// Supported contexts
|
||||
List<PluginContextType> supportedContexts;
|
||||
|
||||
/// Filter title
|
||||
String title;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginFilterResponseDto &&
|
||||
other.description == description &&
|
||||
other.id == id &&
|
||||
other.methodName == methodName &&
|
||||
other.pluginId == pluginId &&
|
||||
other.schema == schema &&
|
||||
_deepEquality.equals(other.supportedContexts, supportedContexts) &&
|
||||
other.title == title;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(description.hashCode) +
|
||||
(id.hashCode) +
|
||||
(methodName.hashCode) +
|
||||
(pluginId.hashCode) +
|
||||
(schema == null ? 0 : schema!.hashCode) +
|
||||
(supportedContexts.hashCode) +
|
||||
(title.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PluginFilterResponseDto[description=$description, id=$id, methodName=$methodName, pluginId=$pluginId, schema=$schema, supportedContexts=$supportedContexts, title=$title]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'description'] = this.description;
|
||||
json[r'id'] = this.id;
|
||||
json[r'methodName'] = this.methodName;
|
||||
json[r'pluginId'] = this.pluginId;
|
||||
if (this.schema != null) {
|
||||
json[r'schema'] = this.schema;
|
||||
} else {
|
||||
// json[r'schema'] = null;
|
||||
}
|
||||
json[r'supportedContexts'] = this.supportedContexts;
|
||||
json[r'title'] = this.title;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PluginFilterResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PluginFilterResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PluginFilterResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PluginFilterResponseDto(
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
methodName: mapValueOfType<String>(json, r'methodName')!,
|
||||
pluginId: mapValueOfType<String>(json, r'pluginId')!,
|
||||
schema: mapValueOfType<Object>(json, r'schema'),
|
||||
supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PluginFilterResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginFilterResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginFilterResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PluginFilterResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginFilterResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PluginFilterResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PluginFilterResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PluginFilterResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PluginFilterResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PluginFilterResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'description',
|
||||
'id',
|
||||
'methodName',
|
||||
'pluginId',
|
||||
'schema',
|
||||
'supportedContexts',
|
||||
'title',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,105 +10,97 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PluginActionResponseDto {
|
||||
/// Returns a new [PluginActionResponseDto] instance.
|
||||
PluginActionResponseDto({
|
||||
class PluginMethodResponseDto {
|
||||
/// Returns a new [PluginMethodResponseDto] instance.
|
||||
PluginMethodResponseDto({
|
||||
required this.description,
|
||||
required this.id,
|
||||
required this.methodName,
|
||||
required this.pluginId,
|
||||
required this.key,
|
||||
required this.name,
|
||||
required this.schema,
|
||||
this.supportedContexts = const [],
|
||||
required this.title,
|
||||
this.types = const [],
|
||||
});
|
||||
|
||||
/// Action description
|
||||
/// Description
|
||||
String description;
|
||||
|
||||
/// Action ID
|
||||
String id;
|
||||
/// Key
|
||||
String key;
|
||||
|
||||
/// Method name
|
||||
String methodName;
|
||||
/// Name
|
||||
String name;
|
||||
|
||||
/// Plugin ID
|
||||
String pluginId;
|
||||
|
||||
/// Action schema
|
||||
/// Schema
|
||||
Object? schema;
|
||||
|
||||
/// Supported contexts
|
||||
List<PluginContextType> supportedContexts;
|
||||
|
||||
/// Action title
|
||||
/// Title
|
||||
String title;
|
||||
|
||||
/// Workflow types
|
||||
List<WorkflowType> types;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginActionResponseDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginMethodResponseDto &&
|
||||
other.description == description &&
|
||||
other.id == id &&
|
||||
other.methodName == methodName &&
|
||||
other.pluginId == pluginId &&
|
||||
other.key == key &&
|
||||
other.name == name &&
|
||||
other.schema == schema &&
|
||||
_deepEquality.equals(other.supportedContexts, supportedContexts) &&
|
||||
other.title == title;
|
||||
other.title == title &&
|
||||
_deepEquality.equals(other.types, types);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(description.hashCode) +
|
||||
(id.hashCode) +
|
||||
(methodName.hashCode) +
|
||||
(pluginId.hashCode) +
|
||||
(key.hashCode) +
|
||||
(name.hashCode) +
|
||||
(schema == null ? 0 : schema!.hashCode) +
|
||||
(supportedContexts.hashCode) +
|
||||
(title.hashCode);
|
||||
(title.hashCode) +
|
||||
(types.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PluginActionResponseDto[description=$description, id=$id, methodName=$methodName, pluginId=$pluginId, schema=$schema, supportedContexts=$supportedContexts, title=$title]';
|
||||
String toString() => 'PluginMethodResponseDto[description=$description, key=$key, name=$name, schema=$schema, title=$title, types=$types]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'description'] = this.description;
|
||||
json[r'id'] = this.id;
|
||||
json[r'methodName'] = this.methodName;
|
||||
json[r'pluginId'] = this.pluginId;
|
||||
json[r'key'] = this.key;
|
||||
json[r'name'] = this.name;
|
||||
if (this.schema != null) {
|
||||
json[r'schema'] = this.schema;
|
||||
} else {
|
||||
// json[r'schema'] = null;
|
||||
}
|
||||
json[r'supportedContexts'] = this.supportedContexts;
|
||||
json[r'title'] = this.title;
|
||||
json[r'types'] = this.types;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PluginActionResponseDto] instance and imports its values from
|
||||
/// Returns a new [PluginMethodResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PluginActionResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PluginActionResponseDto");
|
||||
static PluginMethodResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PluginMethodResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PluginActionResponseDto(
|
||||
return PluginMethodResponseDto(
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
methodName: mapValueOfType<String>(json, r'methodName')!,
|
||||
pluginId: mapValueOfType<String>(json, r'pluginId')!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
schema: mapValueOfType<Object>(json, r'schema'),
|
||||
supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
types: WorkflowType.listFromJson(json[r'types']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PluginActionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginActionResponseDto>[];
|
||||
static List<PluginMethodResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginMethodResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginActionResponseDto.fromJson(row);
|
||||
final value = PluginMethodResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -117,12 +109,12 @@ class PluginActionResponseDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PluginActionResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginActionResponseDto>{};
|
||||
static Map<String, PluginMethodResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginMethodResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PluginActionResponseDto.fromJson(entry.value);
|
||||
final value = PluginMethodResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -131,14 +123,14 @@ class PluginActionResponseDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PluginActionResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PluginActionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PluginActionResponseDto>>{};
|
||||
// maps a json object with a list of PluginMethodResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PluginMethodResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PluginMethodResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PluginActionResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = PluginMethodResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@@ -147,12 +139,11 @@ class PluginActionResponseDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'description',
|
||||
'id',
|
||||
'methodName',
|
||||
'pluginId',
|
||||
'key',
|
||||
'name',
|
||||
'schema',
|
||||
'supportedContexts',
|
||||
'title',
|
||||
'types',
|
||||
};
|
||||
}
|
||||
|
||||
29
mobile/openapi/lib/model/plugin_response_dto.dart
generated
29
mobile/openapi/lib/model/plugin_response_dto.dart
generated
@@ -13,21 +13,17 @@ part of openapi.api;
|
||||
class PluginResponseDto {
|
||||
/// Returns a new [PluginResponseDto] instance.
|
||||
PluginResponseDto({
|
||||
this.actions = const [],
|
||||
required this.author,
|
||||
required this.createdAt,
|
||||
required this.description,
|
||||
this.filters = const [],
|
||||
required this.id,
|
||||
this.methods = const [],
|
||||
required this.name,
|
||||
required this.title,
|
||||
required this.updatedAt,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
/// Plugin actions
|
||||
List<PluginActionResponseDto> actions;
|
||||
|
||||
/// Plugin author
|
||||
String author;
|
||||
|
||||
@@ -37,12 +33,12 @@ class PluginResponseDto {
|
||||
/// Plugin description
|
||||
String description;
|
||||
|
||||
/// Plugin filters
|
||||
List<PluginFilterResponseDto> filters;
|
||||
|
||||
/// Plugin ID
|
||||
String id;
|
||||
|
||||
/// Plugin methods
|
||||
List<PluginMethodResponseDto> methods;
|
||||
|
||||
/// Plugin name
|
||||
String name;
|
||||
|
||||
@@ -57,12 +53,11 @@ class PluginResponseDto {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginResponseDto &&
|
||||
_deepEquality.equals(other.actions, actions) &&
|
||||
other.author == author &&
|
||||
other.createdAt == createdAt &&
|
||||
other.description == description &&
|
||||
_deepEquality.equals(other.filters, filters) &&
|
||||
other.id == id &&
|
||||
_deepEquality.equals(other.methods, methods) &&
|
||||
other.name == name &&
|
||||
other.title == title &&
|
||||
other.updatedAt == updatedAt &&
|
||||
@@ -71,28 +66,26 @@ class PluginResponseDto {
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(author.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(id.hashCode) +
|
||||
(methods.hashCode) +
|
||||
(name.hashCode) +
|
||||
(title.hashCode) +
|
||||
(updatedAt.hashCode) +
|
||||
(version.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PluginResponseDto[actions=$actions, author=$author, createdAt=$createdAt, description=$description, filters=$filters, id=$id, name=$name, title=$title, updatedAt=$updatedAt, version=$version]';
|
||||
String toString() => 'PluginResponseDto[author=$author, createdAt=$createdAt, description=$description, id=$id, methods=$methods, name=$name, title=$title, updatedAt=$updatedAt, version=$version]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
json[r'author'] = this.author;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'description'] = this.description;
|
||||
json[r'filters'] = this.filters;
|
||||
json[r'id'] = this.id;
|
||||
json[r'methods'] = this.methods;
|
||||
json[r'name'] = this.name;
|
||||
json[r'title'] = this.title;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
@@ -109,12 +102,11 @@ class PluginResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PluginResponseDto(
|
||||
actions: PluginActionResponseDto.listFromJson(json[r'actions']),
|
||||
author: mapValueOfType<String>(json, r'author')!,
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
filters: PluginFilterResponseDto.listFromJson(json[r'filters']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
methods: PluginMethodResponseDto.listFromJson(json[r'methods']),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
@@ -166,12 +158,11 @@ class PluginResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actions',
|
||||
'author',
|
||||
'createdAt',
|
||||
'description',
|
||||
'filters',
|
||||
'id',
|
||||
'methods',
|
||||
'name',
|
||||
'title',
|
||||
'updatedAt',
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PluginTriggerResponseDto {
|
||||
/// Returns a new [PluginTriggerResponseDto] instance.
|
||||
PluginTriggerResponseDto({
|
||||
required this.contextType,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
/// Context type
|
||||
PluginContextType contextType;
|
||||
|
||||
/// Trigger type
|
||||
PluginTriggerType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto &&
|
||||
other.contextType == contextType &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(contextType.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PluginTriggerResponseDto[contextType=$contextType, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'contextType'] = this.contextType;
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PluginTriggerResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PluginTriggerResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PluginTriggerResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PluginTriggerResponseDto(
|
||||
contextType: PluginContextType.fromJson(json[r'contextType'])!,
|
||||
type: PluginTriggerType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PluginTriggerResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginTriggerResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginTriggerResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PluginTriggerResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginTriggerResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PluginTriggerResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PluginTriggerResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PluginTriggerResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PluginTriggerResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PluginTriggerResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'contextType',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/plugin_trigger_type.dart
generated
85
mobile/openapi/lib/model/plugin_trigger_type.dart
generated
@@ -1,85 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Trigger type
|
||||
class PluginTriggerType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const PluginTriggerType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const assetCreate = PluginTriggerType._(r'AssetCreate');
|
||||
static const personRecognized = PluginTriggerType._(r'PersonRecognized');
|
||||
|
||||
/// List of all possible values in this [enum][PluginTriggerType].
|
||||
static const values = <PluginTriggerType>[
|
||||
assetCreate,
|
||||
personRecognized,
|
||||
];
|
||||
|
||||
static PluginTriggerType? fromJson(dynamic value) => PluginTriggerTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<PluginTriggerType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginTriggerType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginTriggerType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [PluginTriggerType] to String,
|
||||
/// and [decode] dynamic data back to [PluginTriggerType].
|
||||
class PluginTriggerTypeTypeTransformer {
|
||||
factory PluginTriggerTypeTypeTransformer() => _instance ??= const PluginTriggerTypeTypeTransformer._();
|
||||
|
||||
const PluginTriggerTypeTypeTransformer._();
|
||||
|
||||
String encode(PluginTriggerType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a PluginTriggerType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
PluginTriggerType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'AssetCreate': return PluginTriggerType.assetCreate;
|
||||
case r'PersonRecognized': return PluginTriggerType.personRecognized;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [PluginTriggerTypeTypeTransformer] instance.
|
||||
static PluginTriggerTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
118
mobile/openapi/lib/model/workflow_action_item_dto.dart
generated
118
mobile/openapi/lib/model/workflow_action_item_dto.dart
generated
@@ -1,118 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowActionItemDto {
|
||||
/// Returns a new [WorkflowActionItemDto] instance.
|
||||
WorkflowActionItemDto({
|
||||
this.actionConfig,
|
||||
required this.pluginActionId,
|
||||
});
|
||||
|
||||
/// Action configuration
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
Object? actionConfig;
|
||||
|
||||
/// Plugin action ID
|
||||
String pluginActionId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto &&
|
||||
other.actionConfig == actionConfig &&
|
||||
other.pluginActionId == pluginActionId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actionConfig == null ? 0 : actionConfig!.hashCode) +
|
||||
(pluginActionId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowActionItemDto[actionConfig=$actionConfig, pluginActionId=$pluginActionId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.actionConfig != null) {
|
||||
json[r'actionConfig'] = this.actionConfig;
|
||||
} else {
|
||||
// json[r'actionConfig'] = null;
|
||||
}
|
||||
json[r'pluginActionId'] = this.pluginActionId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowActionItemDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowActionItemDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowActionItemDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowActionItemDto(
|
||||
actionConfig: mapValueOfType<Object>(json, r'actionConfig'),
|
||||
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowActionItemDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowActionItemDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowActionItemDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowActionItemDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowActionItemDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowActionItemDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowActionItemDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowActionItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowActionItemDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowActionItemDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'pluginActionId',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowActionResponseDto {
|
||||
/// Returns a new [WorkflowActionResponseDto] instance.
|
||||
WorkflowActionResponseDto({
|
||||
required this.actionConfig,
|
||||
required this.id,
|
||||
required this.order,
|
||||
required this.pluginActionId,
|
||||
required this.workflowId,
|
||||
});
|
||||
|
||||
/// Action configuration
|
||||
Object? actionConfig;
|
||||
|
||||
/// Action ID
|
||||
String id;
|
||||
|
||||
/// Action order
|
||||
num order;
|
||||
|
||||
/// Plugin action ID
|
||||
String pluginActionId;
|
||||
|
||||
/// Workflow ID
|
||||
String workflowId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto &&
|
||||
other.actionConfig == actionConfig &&
|
||||
other.id == id &&
|
||||
other.order == order &&
|
||||
other.pluginActionId == pluginActionId &&
|
||||
other.workflowId == workflowId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actionConfig == null ? 0 : actionConfig!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(order.hashCode) +
|
||||
(pluginActionId.hashCode) +
|
||||
(workflowId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowActionResponseDto[actionConfig=$actionConfig, id=$id, order=$order, pluginActionId=$pluginActionId, workflowId=$workflowId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.actionConfig != null) {
|
||||
json[r'actionConfig'] = this.actionConfig;
|
||||
} else {
|
||||
// json[r'actionConfig'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'order'] = this.order;
|
||||
json[r'pluginActionId'] = this.pluginActionId;
|
||||
json[r'workflowId'] = this.workflowId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowActionResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowActionResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowActionResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowActionResponseDto(
|
||||
actionConfig: mapValueOfType<Object>(json, r'actionConfig'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
order: num.parse('${json[r'order']}'),
|
||||
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
|
||||
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowActionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowActionResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowActionResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowActionResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowActionResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowActionResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowActionResponseDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowActionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowActionResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowActionResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actionConfig',
|
||||
'id',
|
||||
'order',
|
||||
'pluginActionId',
|
||||
'workflowId',
|
||||
};
|
||||
}
|
||||
|
||||
60
mobile/openapi/lib/model/workflow_create_dto.dart
generated
60
mobile/openapi/lib/model/workflow_create_dto.dart
generated
@@ -13,24 +13,14 @@ part of openapi.api;
|
||||
class WorkflowCreateDto {
|
||||
/// Returns a new [WorkflowCreateDto] instance.
|
||||
WorkflowCreateDto({
|
||||
this.actions = const [],
|
||||
this.description,
|
||||
this.enabled,
|
||||
this.filters = const [],
|
||||
required this.name,
|
||||
required this.triggerType,
|
||||
this.name,
|
||||
this.steps = const [],
|
||||
required this.trigger,
|
||||
});
|
||||
|
||||
/// Workflow actions
|
||||
List<WorkflowActionItemDto> actions;
|
||||
|
||||
/// Workflow description
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? description;
|
||||
|
||||
/// Workflow enabled
|
||||
@@ -42,40 +32,36 @@ class WorkflowCreateDto {
|
||||
///
|
||||
bool? enabled;
|
||||
|
||||
/// Workflow filters
|
||||
List<WorkflowFilterItemDto> filters;
|
||||
|
||||
/// Workflow name
|
||||
String name;
|
||||
String? name;
|
||||
|
||||
List<WorkflowStepDto> steps;
|
||||
|
||||
/// Workflow trigger type
|
||||
PluginTriggerType triggerType;
|
||||
WorkflowTrigger trigger;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowCreateDto &&
|
||||
_deepEquality.equals(other.actions, actions) &&
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
_deepEquality.equals(other.filters, filters) &&
|
||||
other.name == name &&
|
||||
other.triggerType == triggerType;
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(name.hashCode) +
|
||||
(triggerType.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(steps.hashCode) +
|
||||
(trigger.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowCreateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]';
|
||||
String toString() => 'WorkflowCreateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
@@ -86,9 +72,13 @@ class WorkflowCreateDto {
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
json[r'filters'] = this.filters;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
json[r'triggerType'] = this.triggerType;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
json[r'steps'] = this.steps;
|
||||
json[r'trigger'] = this.trigger;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -101,12 +91,11 @@ class WorkflowCreateDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowCreateDto(
|
||||
actions: WorkflowActionItemDto.listFromJson(json[r'actions']),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||
filters: WorkflowFilterItemDto.listFromJson(json[r'filters']),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!,
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
steps: WorkflowStepDto.listFromJson(json[r'steps']),
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -154,10 +143,7 @@ class WorkflowCreateDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actions',
|
||||
'filters',
|
||||
'name',
|
||||
'triggerType',
|
||||
'trigger',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
118
mobile/openapi/lib/model/workflow_filter_item_dto.dart
generated
118
mobile/openapi/lib/model/workflow_filter_item_dto.dart
generated
@@ -1,118 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowFilterItemDto {
|
||||
/// Returns a new [WorkflowFilterItemDto] instance.
|
||||
WorkflowFilterItemDto({
|
||||
this.filterConfig,
|
||||
required this.pluginFilterId,
|
||||
});
|
||||
|
||||
/// Filter configuration
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
Object? filterConfig;
|
||||
|
||||
/// Plugin filter ID
|
||||
String pluginFilterId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto &&
|
||||
other.filterConfig == filterConfig &&
|
||||
other.pluginFilterId == pluginFilterId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(filterConfig == null ? 0 : filterConfig!.hashCode) +
|
||||
(pluginFilterId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowFilterItemDto[filterConfig=$filterConfig, pluginFilterId=$pluginFilterId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.filterConfig != null) {
|
||||
json[r'filterConfig'] = this.filterConfig;
|
||||
} else {
|
||||
// json[r'filterConfig'] = null;
|
||||
}
|
||||
json[r'pluginFilterId'] = this.pluginFilterId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowFilterItemDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowFilterItemDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowFilterItemDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowFilterItemDto(
|
||||
filterConfig: mapValueOfType<Object>(json, r'filterConfig'),
|
||||
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowFilterItemDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowFilterItemDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowFilterItemDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowFilterItemDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowFilterItemDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowFilterItemDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowFilterItemDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowFilterItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowFilterItemDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowFilterItemDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'pluginFilterId',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowFilterResponseDto {
|
||||
/// Returns a new [WorkflowFilterResponseDto] instance.
|
||||
WorkflowFilterResponseDto({
|
||||
required this.filterConfig,
|
||||
required this.id,
|
||||
required this.order,
|
||||
required this.pluginFilterId,
|
||||
required this.workflowId,
|
||||
});
|
||||
|
||||
/// Filter configuration
|
||||
Object? filterConfig;
|
||||
|
||||
/// Filter ID
|
||||
String id;
|
||||
|
||||
/// Filter order
|
||||
num order;
|
||||
|
||||
/// Plugin filter ID
|
||||
String pluginFilterId;
|
||||
|
||||
/// Workflow ID
|
||||
String workflowId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto &&
|
||||
other.filterConfig == filterConfig &&
|
||||
other.id == id &&
|
||||
other.order == order &&
|
||||
other.pluginFilterId == pluginFilterId &&
|
||||
other.workflowId == workflowId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(filterConfig == null ? 0 : filterConfig!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(order.hashCode) +
|
||||
(pluginFilterId.hashCode) +
|
||||
(workflowId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowFilterResponseDto[filterConfig=$filterConfig, id=$id, order=$order, pluginFilterId=$pluginFilterId, workflowId=$workflowId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.filterConfig != null) {
|
||||
json[r'filterConfig'] = this.filterConfig;
|
||||
} else {
|
||||
// json[r'filterConfig'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'order'] = this.order;
|
||||
json[r'pluginFilterId'] = this.pluginFilterId;
|
||||
json[r'workflowId'] = this.workflowId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowFilterResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowFilterResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowFilterResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowFilterResponseDto(
|
||||
filterConfig: mapValueOfType<Object>(json, r'filterConfig'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
order: num.parse('${json[r'order']}'),
|
||||
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
|
||||
workflowId: mapValueOfType<String>(json, r'workflowId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowFilterResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowFilterResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowFilterResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowFilterResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowFilterResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowFilterResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowFilterResponseDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowFilterResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowFilterResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowFilterResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'filterConfig',
|
||||
'id',
|
||||
'order',
|
||||
'pluginFilterId',
|
||||
'workflowId',
|
||||
};
|
||||
}
|
||||
|
||||
69
mobile/openapi/lib/model/workflow_response_dto.dart
generated
69
mobile/openapi/lib/model/workflow_response_dto.dart
generated
@@ -13,87 +13,84 @@ part of openapi.api;
|
||||
class WorkflowResponseDto {
|
||||
/// Returns a new [WorkflowResponseDto] instance.
|
||||
WorkflowResponseDto({
|
||||
this.actions = const [],
|
||||
required this.createdAt,
|
||||
required this.description,
|
||||
required this.enabled,
|
||||
this.filters = const [],
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.ownerId,
|
||||
required this.triggerType,
|
||||
this.steps = const [],
|
||||
required this.trigger,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Workflow actions
|
||||
List<WorkflowActionResponseDto> actions;
|
||||
|
||||
/// Creation date
|
||||
String createdAt;
|
||||
|
||||
/// Workflow description
|
||||
String description;
|
||||
String? description;
|
||||
|
||||
/// Workflow enabled
|
||||
bool enabled;
|
||||
|
||||
/// Workflow filters
|
||||
List<WorkflowFilterResponseDto> filters;
|
||||
|
||||
/// Workflow ID
|
||||
String id;
|
||||
|
||||
/// Workflow name
|
||||
String? name;
|
||||
|
||||
/// Owner user ID
|
||||
String ownerId;
|
||||
/// Workflow steps
|
||||
List<WorkflowStepResponseDto> steps;
|
||||
|
||||
/// Workflow trigger type
|
||||
PluginTriggerType triggerType;
|
||||
WorkflowTrigger trigger;
|
||||
|
||||
/// Update date
|
||||
String updatedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowResponseDto &&
|
||||
_deepEquality.equals(other.actions, actions) &&
|
||||
other.createdAt == createdAt &&
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
_deepEquality.equals(other.filters, filters) &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
other.ownerId == ownerId &&
|
||||
other.triggerType == triggerType;
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(triggerType.hashCode);
|
||||
(steps.hashCode) +
|
||||
(trigger.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowResponseDto[actions=$actions, createdAt=$createdAt, description=$description, enabled=$enabled, filters=$filters, id=$id, name=$name, ownerId=$ownerId, triggerType=$triggerType]';
|
||||
String toString() => 'WorkflowResponseDto[createdAt=$createdAt, description=$description, enabled=$enabled, id=$id, name=$name, steps=$steps, trigger=$trigger, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'filters'] = this.filters;
|
||||
json[r'id'] = this.id;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'triggerType'] = this.triggerType;
|
||||
json[r'steps'] = this.steps;
|
||||
json[r'trigger'] = this.trigger;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -106,15 +103,14 @@ class WorkflowResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowResponseDto(
|
||||
actions: WorkflowActionResponseDto.listFromJson(json[r'actions']),
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
description: mapValueOfType<String>(json, r'description')!,
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
filters: WorkflowFilterResponseDto.listFromJson(json[r'filters']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!,
|
||||
steps: WorkflowStepResponseDto.listFromJson(json[r'steps']),
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -162,15 +158,14 @@ class WorkflowResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actions',
|
||||
'createdAt',
|
||||
'description',
|
||||
'enabled',
|
||||
'filters',
|
||||
'id',
|
||||
'name',
|
||||
'ownerId',
|
||||
'triggerType',
|
||||
'steps',
|
||||
'trigger',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
130
mobile/openapi/lib/model/workflow_step_dto.dart
generated
Normal file
130
mobile/openapi/lib/model/workflow_step_dto.dart
generated
Normal file
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowStepDto {
|
||||
/// Returns a new [WorkflowStepDto] instance.
|
||||
WorkflowStepDto({
|
||||
this.config,
|
||||
this.enabled,
|
||||
required this.method,
|
||||
});
|
||||
|
||||
/// Step configuration
|
||||
Object? config;
|
||||
|
||||
/// Step is enabled
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? enabled;
|
||||
|
||||
/// Step plugin method
|
||||
String method;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowStepDto &&
|
||||
other.config == config &&
|
||||
other.enabled == enabled &&
|
||||
other.method == method;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(config == null ? 0 : config!.hashCode) +
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(method.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowStepDto[config=$config, enabled=$enabled, method=$method]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.config != null) {
|
||||
json[r'config'] = this.config;
|
||||
} else {
|
||||
// json[r'config'] = null;
|
||||
}
|
||||
if (this.enabled != null) {
|
||||
json[r'enabled'] = this.enabled;
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
json[r'method'] = this.method;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowStepDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowStepDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowStepDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowStepDto(
|
||||
config: mapValueOfType<Object>(json, r'config'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||
method: mapValueOfType<String>(json, r'method')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowStepDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowStepDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowStepDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowStepDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowStepDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowStepDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowStepDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowStepDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowStepDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowStepDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'method',
|
||||
};
|
||||
}
|
||||
|
||||
122
mobile/openapi/lib/model/workflow_step_response_dto.dart
generated
Normal file
122
mobile/openapi/lib/model/workflow_step_response_dto.dart
generated
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowStepResponseDto {
|
||||
/// Returns a new [WorkflowStepResponseDto] instance.
|
||||
WorkflowStepResponseDto({
|
||||
required this.config,
|
||||
required this.enabled,
|
||||
required this.method,
|
||||
});
|
||||
|
||||
/// Step configuration
|
||||
Object? config;
|
||||
|
||||
/// Step is enabled
|
||||
bool enabled;
|
||||
|
||||
/// Step plugin method
|
||||
String method;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowStepResponseDto &&
|
||||
other.config == config &&
|
||||
other.enabled == enabled &&
|
||||
other.method == method;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(config == null ? 0 : config!.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(method.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowStepResponseDto[config=$config, enabled=$enabled, method=$method]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.config != null) {
|
||||
json[r'config'] = this.config;
|
||||
} else {
|
||||
// json[r'config'] = null;
|
||||
}
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'method'] = this.method;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowStepResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowStepResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowStepResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowStepResponseDto(
|
||||
config: mapValueOfType<Object>(json, r'config'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
method: mapValueOfType<String>(json, r'method')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowStepResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowStepResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowStepResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowStepResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowStepResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowStepResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowStepResponseDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowStepResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowStepResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowStepResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'config',
|
||||
'enabled',
|
||||
'method',
|
||||
};
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/workflow_trigger.dart
generated
Normal file
85
mobile/openapi/lib/model/workflow_trigger.dart
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class WorkflowTrigger {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const WorkflowTrigger._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const assetCreate = WorkflowTrigger._(r'AssetCreate');
|
||||
static const personRecognized = WorkflowTrigger._(r'PersonRecognized');
|
||||
|
||||
/// List of all possible values in this [enum][WorkflowTrigger].
|
||||
static const values = <WorkflowTrigger>[
|
||||
assetCreate,
|
||||
personRecognized,
|
||||
];
|
||||
|
||||
static WorkflowTrigger? fromJson(dynamic value) => WorkflowTriggerTypeTransformer().decode(value);
|
||||
|
||||
static List<WorkflowTrigger> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowTrigger>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowTrigger.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [WorkflowTrigger] to String,
|
||||
/// and [decode] dynamic data back to [WorkflowTrigger].
|
||||
class WorkflowTriggerTypeTransformer {
|
||||
factory WorkflowTriggerTypeTransformer() => _instance ??= const WorkflowTriggerTypeTransformer._();
|
||||
|
||||
const WorkflowTriggerTypeTransformer._();
|
||||
|
||||
String encode(WorkflowTrigger data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a WorkflowTrigger.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
WorkflowTrigger? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'AssetCreate': return WorkflowTrigger.assetCreate;
|
||||
case r'PersonRecognized': return WorkflowTrigger.personRecognized;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [WorkflowTriggerTypeTransformer] instance.
|
||||
static WorkflowTriggerTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
109
mobile/openapi/lib/model/workflow_trigger_response_dto.dart
generated
Normal file
109
mobile/openapi/lib/model/workflow_trigger_response_dto.dart
generated
Normal file
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class WorkflowTriggerResponseDto {
|
||||
/// Returns a new [WorkflowTriggerResponseDto] instance.
|
||||
WorkflowTriggerResponseDto({
|
||||
required this.trigger,
|
||||
this.types = const [],
|
||||
});
|
||||
|
||||
/// Trigger type
|
||||
WorkflowTrigger trigger;
|
||||
|
||||
/// Workflow types
|
||||
List<WorkflowType> types;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowTriggerResponseDto &&
|
||||
other.trigger == trigger &&
|
||||
_deepEquality.equals(other.types, types);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(trigger.hashCode) +
|
||||
(types.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowTriggerResponseDto[trigger=$trigger, types=$types]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'trigger'] = this.trigger;
|
||||
json[r'types'] = this.types;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowTriggerResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowTriggerResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowTriggerResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowTriggerResponseDto(
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
types: WorkflowType.listFromJson(json[r'types']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowTriggerResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowTriggerResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowTriggerResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowTriggerResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowTriggerResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowTriggerResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowTriggerResponseDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowTriggerResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowTriggerResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowTriggerResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'trigger',
|
||||
'types',
|
||||
};
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/workflow_type.dart
generated
Normal file
85
mobile/openapi/lib/model/workflow_type.dart
generated
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Workflow types
|
||||
class WorkflowType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const WorkflowType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const assetV1 = WorkflowType._(r'AssetV1');
|
||||
static const assetPersonV1 = WorkflowType._(r'AssetPersonV1');
|
||||
|
||||
/// List of all possible values in this [enum][WorkflowType].
|
||||
static const values = <WorkflowType>[
|
||||
assetV1,
|
||||
assetPersonV1,
|
||||
];
|
||||
|
||||
static WorkflowType? fromJson(dynamic value) => WorkflowTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<WorkflowType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [WorkflowType] to String,
|
||||
/// and [decode] dynamic data back to [WorkflowType].
|
||||
class WorkflowTypeTypeTransformer {
|
||||
factory WorkflowTypeTypeTransformer() => _instance ??= const WorkflowTypeTypeTransformer._();
|
||||
|
||||
const WorkflowTypeTypeTransformer._();
|
||||
|
||||
String encode(WorkflowType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a WorkflowType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
WorkflowType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'AssetV1': return WorkflowType.assetV1;
|
||||
case r'AssetPersonV1': return WorkflowType.assetPersonV1;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [WorkflowTypeTypeTransformer] instance.
|
||||
static WorkflowTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
53
mobile/openapi/lib/model/workflow_update_dto.dart
generated
53
mobile/openapi/lib/model/workflow_update_dto.dart
generated
@@ -13,24 +13,14 @@ part of openapi.api;
|
||||
class WorkflowUpdateDto {
|
||||
/// Returns a new [WorkflowUpdateDto] instance.
|
||||
WorkflowUpdateDto({
|
||||
this.actions = const [],
|
||||
this.description,
|
||||
this.enabled,
|
||||
this.filters = const [],
|
||||
this.name,
|
||||
this.triggerType,
|
||||
this.steps = const [],
|
||||
this.trigger,
|
||||
});
|
||||
|
||||
/// Workflow actions
|
||||
List<WorkflowActionItemDto> actions;
|
||||
|
||||
/// Workflow description
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? description;
|
||||
|
||||
/// Workflow enabled
|
||||
@@ -42,18 +32,11 @@ class WorkflowUpdateDto {
|
||||
///
|
||||
bool? enabled;
|
||||
|
||||
/// Workflow filters
|
||||
List<WorkflowFilterItemDto> filters;
|
||||
|
||||
/// Workflow name
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? name;
|
||||
|
||||
List<WorkflowStepDto> steps;
|
||||
|
||||
/// Workflow trigger type
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -61,33 +44,30 @@ class WorkflowUpdateDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
PluginTriggerType? triggerType;
|
||||
WorkflowTrigger? trigger;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto &&
|
||||
_deepEquality.equals(other.actions, actions) &&
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
_deepEquality.equals(other.filters, filters) &&
|
||||
other.name == name &&
|
||||
other.triggerType == triggerType;
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(triggerType == null ? 0 : triggerType!.hashCode);
|
||||
(steps.hashCode) +
|
||||
(trigger == null ? 0 : trigger!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]';
|
||||
String toString() => 'WorkflowUpdateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
@@ -98,16 +78,16 @@ class WorkflowUpdateDto {
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
json[r'filters'] = this.filters;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.triggerType != null) {
|
||||
json[r'triggerType'] = this.triggerType;
|
||||
json[r'steps'] = this.steps;
|
||||
if (this.trigger != null) {
|
||||
json[r'trigger'] = this.trigger;
|
||||
} else {
|
||||
// json[r'triggerType'] = null;
|
||||
// json[r'trigger'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
@@ -121,12 +101,11 @@ class WorkflowUpdateDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowUpdateDto(
|
||||
actions: WorkflowActionItemDto.listFromJson(json[r'actions']),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled'),
|
||||
filters: WorkflowFilterItemDto.listFromJson(json[r'filters']),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
triggerType: PluginTriggerType.fromJson(json[r'triggerType']),
|
||||
steps: WorkflowStepDto.listFromJson(json[r'steps']),
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1561,51 +1561,31 @@ export type PersonStatisticsResponseDto = {
|
||||
/** Number of assets */
|
||||
assets: number;
|
||||
};
|
||||
export type PluginActionResponseDto = {
|
||||
/** Action description */
|
||||
export type PluginMethodResponseDto = {
|
||||
/** Description */
|
||||
description: string;
|
||||
/** Action ID */
|
||||
id: string;
|
||||
/** Method name */
|
||||
methodName: string;
|
||||
/** Plugin ID */
|
||||
pluginId: string;
|
||||
/** Action schema */
|
||||
/** Key */
|
||||
key: string;
|
||||
/** Name */
|
||||
name: string;
|
||||
/** Schema */
|
||||
schema: object | null;
|
||||
/** Supported contexts */
|
||||
supportedContexts: PluginContextType[];
|
||||
/** Action title */
|
||||
title: string;
|
||||
};
|
||||
export type PluginFilterResponseDto = {
|
||||
/** Filter description */
|
||||
description: string;
|
||||
/** Filter ID */
|
||||
id: string;
|
||||
/** Method name */
|
||||
methodName: string;
|
||||
/** Plugin ID */
|
||||
pluginId: string;
|
||||
/** Filter schema */
|
||||
schema: object | null;
|
||||
/** Supported contexts */
|
||||
supportedContexts: PluginContextType[];
|
||||
/** Filter title */
|
||||
/** Title */
|
||||
title: string;
|
||||
/** Workflow types */
|
||||
types: WorkflowType[];
|
||||
};
|
||||
export type PluginResponseDto = {
|
||||
/** Plugin actions */
|
||||
actions: PluginActionResponseDto[];
|
||||
/** Plugin author */
|
||||
author: string;
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Plugin description */
|
||||
description: string;
|
||||
/** Plugin filters */
|
||||
filters: PluginFilterResponseDto[];
|
||||
/** Plugin ID */
|
||||
id: string;
|
||||
/** Plugin methods */
|
||||
methods: PluginMethodResponseDto[];
|
||||
/** Plugin name */
|
||||
name: string;
|
||||
/** Plugin title */
|
||||
@@ -1615,12 +1595,6 @@ export type PluginResponseDto = {
|
||||
/** Plugin version */
|
||||
version: string;
|
||||
};
|
||||
export type PluginTriggerResponseDto = {
|
||||
/** Context type */
|
||||
contextType: PluginContextType;
|
||||
/** Trigger type */
|
||||
"type": PluginTriggerType;
|
||||
};
|
||||
export type QueueResponseDto = {
|
||||
/** Whether the queue is paused */
|
||||
isPaused: boolean;
|
||||
@@ -2829,89 +2803,67 @@ export type CreateProfileImageResponseDto = {
|
||||
/** User ID */
|
||||
userId: string;
|
||||
};
|
||||
export type WorkflowActionResponseDto = {
|
||||
/** Action configuration */
|
||||
actionConfig: object | null;
|
||||
/** Action ID */
|
||||
id: string;
|
||||
/** Action order */
|
||||
order: number;
|
||||
/** Plugin action ID */
|
||||
pluginActionId: string;
|
||||
/** Workflow ID */
|
||||
workflowId: string;
|
||||
};
|
||||
export type WorkflowFilterResponseDto = {
|
||||
/** Filter configuration */
|
||||
filterConfig: object | null;
|
||||
/** Filter ID */
|
||||
id: string;
|
||||
/** Filter order */
|
||||
order: number;
|
||||
/** Plugin filter ID */
|
||||
pluginFilterId: string;
|
||||
/** Workflow ID */
|
||||
workflowId: string;
|
||||
export type WorkflowStepResponseDto = {
|
||||
/** Step configuration */
|
||||
config: object | null;
|
||||
/** Step is enabled */
|
||||
enabled: boolean;
|
||||
/** Step plugin method */
|
||||
method: string;
|
||||
};
|
||||
export type WorkflowResponseDto = {
|
||||
/** Workflow actions */
|
||||
actions: WorkflowActionResponseDto[];
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Workflow description */
|
||||
description: string;
|
||||
description: string | null;
|
||||
/** Workflow enabled */
|
||||
enabled: boolean;
|
||||
/** Workflow filters */
|
||||
filters: WorkflowFilterResponseDto[];
|
||||
/** Workflow ID */
|
||||
id: string;
|
||||
/** Workflow name */
|
||||
name: string | null;
|
||||
/** Owner user ID */
|
||||
ownerId: string;
|
||||
/** Workflow steps */
|
||||
steps: WorkflowStepResponseDto[];
|
||||
/** Workflow trigger type */
|
||||
triggerType: PluginTriggerType;
|
||||
trigger: WorkflowTrigger;
|
||||
/** Update date */
|
||||
updatedAt: string;
|
||||
};
|
||||
export type WorkflowActionItemDto = {
|
||||
/** Action configuration */
|
||||
actionConfig?: object;
|
||||
/** Plugin action ID */
|
||||
pluginActionId: string;
|
||||
};
|
||||
export type WorkflowFilterItemDto = {
|
||||
/** Filter configuration */
|
||||
filterConfig?: object;
|
||||
/** Plugin filter ID */
|
||||
pluginFilterId: string;
|
||||
export type WorkflowStepDto = {
|
||||
/** Step configuration */
|
||||
config?: object | null;
|
||||
/** Step is enabled */
|
||||
enabled?: boolean;
|
||||
/** Step plugin method */
|
||||
method: string;
|
||||
};
|
||||
export type WorkflowCreateDto = {
|
||||
/** Workflow actions */
|
||||
actions: WorkflowActionItemDto[];
|
||||
/** Workflow description */
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
/** Workflow enabled */
|
||||
enabled?: boolean;
|
||||
/** Workflow filters */
|
||||
filters: WorkflowFilterItemDto[];
|
||||
/** Workflow name */
|
||||
name: string;
|
||||
name?: string | null;
|
||||
steps?: WorkflowStepDto[];
|
||||
/** Workflow trigger type */
|
||||
triggerType: PluginTriggerType;
|
||||
trigger: WorkflowTrigger;
|
||||
};
|
||||
export type WorkflowTriggerResponseDto = {
|
||||
/** Trigger type */
|
||||
trigger: WorkflowTrigger;
|
||||
/** Workflow types */
|
||||
types: WorkflowType[];
|
||||
};
|
||||
export type WorkflowUpdateDto = {
|
||||
/** Workflow actions */
|
||||
actions?: WorkflowActionItemDto[];
|
||||
/** Workflow description */
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
/** Workflow enabled */
|
||||
enabled?: boolean;
|
||||
/** Workflow filters */
|
||||
filters?: WorkflowFilterItemDto[];
|
||||
/** Workflow name */
|
||||
name?: string;
|
||||
name?: string | null;
|
||||
steps?: WorkflowStepDto[];
|
||||
/** Workflow trigger type */
|
||||
triggerType?: PluginTriggerType;
|
||||
trigger?: WorkflowTrigger;
|
||||
};
|
||||
export type SyncAckV1 = {};
|
||||
export type SyncAlbumDeleteV1 = {
|
||||
@@ -5309,22 +5261,56 @@ export function getPersonThumbnail({ id }: {
|
||||
/**
|
||||
* List all plugins
|
||||
*/
|
||||
export function getPlugins(opts?: Oazapfts.RequestOpts) {
|
||||
export function searchPlugins({ description, enabled, id, name, title, version }: {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
version?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PluginResponseDto[];
|
||||
}>("/plugins", {
|
||||
}>(`/plugins${QS.query(QS.explode({
|
||||
description,
|
||||
enabled,
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
version
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* List all plugin triggers
|
||||
* Retrieve plugin methods
|
||||
*/
|
||||
export function getPluginTriggers(opts?: Oazapfts.RequestOpts) {
|
||||
export function searchPluginMethods({ description, enabled, id, name, pluginName, pluginVersion, title, trigger, $type }: {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
pluginName?: string;
|
||||
pluginVersion?: string;
|
||||
title?: string;
|
||||
trigger?: WorkflowTrigger;
|
||||
$type?: WorkflowType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PluginTriggerResponseDto[];
|
||||
}>("/plugins/triggers", {
|
||||
data: PluginMethodResponseDto[];
|
||||
}>(`/plugins/methods${QS.query(QS.explode({
|
||||
description,
|
||||
enabled,
|
||||
id,
|
||||
name,
|
||||
pluginName,
|
||||
pluginVersion,
|
||||
title,
|
||||
trigger,
|
||||
"type": $type
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
@@ -6753,11 +6739,23 @@ export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) {
|
||||
/**
|
||||
* List all workflows
|
||||
*/
|
||||
export function getWorkflows(opts?: Oazapfts.RequestOpts) {
|
||||
export function searchWorkflows({ description, enabled, id, name, trigger }: {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
trigger?: WorkflowTrigger;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowResponseDto[];
|
||||
}>("/workflows", {
|
||||
}>(`/workflows${QS.query(QS.explode({
|
||||
description,
|
||||
enabled,
|
||||
id,
|
||||
name,
|
||||
trigger
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
@@ -6776,6 +6774,17 @@ export function createWorkflow({ workflowCreateDto }: {
|
||||
body: workflowCreateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* List all workflow triggers
|
||||
*/
|
||||
export function getWorkflowTriggers(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowTriggerResponseDto[];
|
||||
}>("/workflows/triggers", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Delete a workflow
|
||||
*/
|
||||
@@ -7145,12 +7154,11 @@ export enum PartnerDirection {
|
||||
SharedBy = "shared-by",
|
||||
SharedWith = "shared-with"
|
||||
}
|
||||
export enum PluginContextType {
|
||||
Asset = "asset",
|
||||
Album = "album",
|
||||
Person = "person"
|
||||
export enum WorkflowType {
|
||||
AssetV1 = "AssetV1",
|
||||
AssetPersonV1 = "AssetPersonV1"
|
||||
}
|
||||
export enum PluginTriggerType {
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = "AssetCreate",
|
||||
PersonRecognized = "PersonRecognized"
|
||||
}
|
||||
@@ -7218,7 +7226,7 @@ export enum JobName {
|
||||
VersionCheck = "VersionCheck",
|
||||
OcrQueueAll = "OcrQueueAll",
|
||||
Ocr = "Ocr",
|
||||
WorkflowRun = "WorkflowRun"
|
||||
WorkflowAssetCreate = "WorkflowAssetCreate"
|
||||
}
|
||||
export enum SearchSuggestionType {
|
||||
Country = "country",
|
||||
|
||||
2
packages/plugin-core/.gitignore
vendored
Normal file
2
packages/plugin-core/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules
|
||||
11
packages/plugin-core/esbuild.js
Normal file
11
packages/plugin-core/esbuild.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
outdir: 'dist',
|
||||
bundle: true,
|
||||
sourcemap: false,
|
||||
minify: false, // might want to use true for production build
|
||||
format: 'cjs', // needs to be CJS for now
|
||||
target: ['es2020'], // don't go over es2020 because quickjs doesn't support it
|
||||
});
|
||||
191
packages/plugin-core/manifest.json
Normal file
191
packages/plugin-core/manifest.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"name": "immich-plugin-core",
|
||||
"version": "2.0.1",
|
||||
"title": "Immich Core Plugin",
|
||||
"description": "Core workflow capabilities for Immich",
|
||||
"author": "Immich Team",
|
||||
"wasmPath": "dist/plugin.wasm",
|
||||
"methods": [
|
||||
{
|
||||
"name": "filterFileName",
|
||||
"title": "Filter by filename",
|
||||
"description": "Filter assets by filename pattern using text matching or regular expressions",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"title": "Filename pattern",
|
||||
"description": "Text or regex pattern to match against filename"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": ["contains", "regex", "exact"],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether matching should be case-sensitive"
|
||||
}
|
||||
},
|
||||
"required": ["pattern"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "filterFileType",
|
||||
"title": "Filter by file type",
|
||||
"description": "Filter assets by file type",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileTypes": {
|
||||
"type": "array",
|
||||
"title": "File types",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["image", "video"]
|
||||
},
|
||||
"description": "Allowed file types"
|
||||
}
|
||||
},
|
||||
"required": ["fileTypes"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "filterPerson",
|
||||
"title": "Filter by person",
|
||||
"description": "Filter by detected person",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"personIds": {
|
||||
"type": "array",
|
||||
"title": "Person IDs",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of person to match",
|
||||
"subType": "people-picker"
|
||||
},
|
||||
"matchAny": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Match any name (true) or require all names (false)"
|
||||
}
|
||||
},
|
||||
"required": ["personIds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetArchive",
|
||||
"title": "Archive",
|
||||
"description": "Move the asset to archive",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {}
|
||||
},
|
||||
{
|
||||
"name": "assetFavorite",
|
||||
"title": "Favorite",
|
||||
"description": "Mark the asset as favorite or unfavorite",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inverse": {
|
||||
"type": "boolean",
|
||||
"title": "Inverse",
|
||||
"description": "Unfavorite by default, set to true to favorite instead",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "albumAddAssets",
|
||||
"title": "Add to Album",
|
||||
"description": "Add the item to a specified album",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"albumId": {
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
},
|
||||
"required": ["albumId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"title": "Test",
|
||||
"description": "Test method with complete configuration examples",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"number1": {
|
||||
"type": "number",
|
||||
"title": "Number 1",
|
||||
"description": "Basic number"
|
||||
},
|
||||
"number2": {
|
||||
"type": "number",
|
||||
"title": "Number 2",
|
||||
"array": true,
|
||||
"description": "List of numbers"
|
||||
},
|
||||
"string1": {
|
||||
"type": "string",
|
||||
"title": "String 1",
|
||||
"description": "Basic string"
|
||||
},
|
||||
"string2": {
|
||||
"type": "string",
|
||||
"title": "String 2",
|
||||
"array": true,
|
||||
"description": "List of strings"
|
||||
},
|
||||
"string3": {
|
||||
"type": "string",
|
||||
"title": "String 3",
|
||||
"enum": ["choice-1", "choice-2"],
|
||||
"description": "Select from a list"
|
||||
},
|
||||
"nested": {
|
||||
"type": "object",
|
||||
"title": "Nested",
|
||||
"description": "Nested properties for nesting",
|
||||
"properties": {
|
||||
"nested1": {
|
||||
"type": "string",
|
||||
"title": "Nested 1",
|
||||
"description": "Nested string"
|
||||
},
|
||||
"nested2": {
|
||||
"type": "number",
|
||||
"title": "Nested 2",
|
||||
"description": "Nested number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"albumId": {
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
},
|
||||
"required": ["albumId"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,4 +8,4 @@ run = "pnpm install --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
depends = ["install"]
|
||||
run = "pnpm run build"
|
||||
run = "pnpm run --filter @immich/plugin-sdk --filter @immich/plugin-core build"
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "plugins",
|
||||
"name": "@immich/plugin-core",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
@@ -14,6 +14,7 @@
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.27.0",
|
||||
"typescript": "^5.3.2"
|
||||
"typescript": "^5.3.2",
|
||||
"@immich/plugin-sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
17
packages/plugin-core/src/index.d.ts
vendored
Normal file
17
packages/plugin-core/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// copy from
|
||||
// import '@immich/plugin-sdk/host-functions';
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
albumAddAssets(ptr: PTR): I64;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'main' {
|
||||
export function assetFileFilter(): I32;
|
||||
export function assetArchive(): I32;
|
||||
export function assetFavorite(): I32;
|
||||
export function assetLock(): I32;
|
||||
export function assetTrash(): I32;
|
||||
export function albumAddAssets(): I32;
|
||||
export function test(): I32;
|
||||
}
|
||||
112
packages/plugin-core/src/index.ts
Normal file
112
packages/plugin-core/src/index.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
AssetStatus,
|
||||
AssetVisibility,
|
||||
WorkflowType,
|
||||
wrapper,
|
||||
} from '@immich/plugin-sdk';
|
||||
|
||||
type AssetFileFilterConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
export const assetFileFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1>(({ data, config }) => {
|
||||
const {
|
||||
pattern,
|
||||
matchType = 'contains',
|
||||
caseSensitive = false,
|
||||
} = config as AssetFileFilterConfig;
|
||||
|
||||
const { asset } = data;
|
||||
|
||||
const fileName = asset.originalFileName || '';
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
if (matchType === 'exact') {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
if (matchType === 'regex') {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(fileName) } };
|
||||
}
|
||||
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
});
|
||||
};
|
||||
|
||||
type AssetArchiveConfig = {
|
||||
inverse?: boolean;
|
||||
};
|
||||
export const assetArchive = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetArchiveConfig>(
|
||||
({ config, data }) => {
|
||||
const target: AssetVisibility = config.inverse
|
||||
? AssetVisibility.Timeline
|
||||
: AssetVisibility.Archive;
|
||||
if (target !== data.asset.visibility) {
|
||||
return {
|
||||
changes: {
|
||||
asset: { visibility: target },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
type AssetFavoriteConfig = {
|
||||
inverse?: boolean;
|
||||
};
|
||||
export const assetFavorite = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetFavoriteConfig>(
|
||||
({ config, data }) => {
|
||||
const target = config.inverse ? false : true;
|
||||
if (target !== data.asset.isFavorite) {
|
||||
return {
|
||||
changes: {
|
||||
asset: { isFavorite: target },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const assetLock = () => {
|
||||
return wrapper<WorkflowType.AssetV1>(() => ({
|
||||
changes: { asset: { visibility: AssetVisibility.Locked } },
|
||||
}));
|
||||
};
|
||||
|
||||
type AssetTrashConfig = {
|
||||
inverse?: boolean;
|
||||
};
|
||||
export const assetTrash = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetTrashConfig>(({ config }) => ({
|
||||
changes: {
|
||||
asset: config.inverse
|
||||
? { deletedAt: null, status: AssetStatus.Active }
|
||||
: { deletedAt: new Date().toISOString(), status: AssetStatus.Trashed },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
type AssetAddToAlbumConfig = {
|
||||
albumId: string;
|
||||
};
|
||||
export const albumAddAssets = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetAddToAlbumConfig>(
|
||||
({ config, data, functions }) => {
|
||||
functions.albumAddAssets(config.albumId, [data.asset.id]);
|
||||
return {};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const test = () => {
|
||||
return wrapper(() => ({}));
|
||||
};
|
||||
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020", // Specify ECMAScript target version
|
||||
"module": "commonjs", // Specify module code generation
|
||||
"lib": [
|
||||
"es2020"
|
||||
], // Specify a list of library files to be included in the compilation
|
||||
"types": [
|
||||
"@extism/js-pdk",
|
||||
"./src/index.d.ts"
|
||||
], // Specify a list of type definition files to be included in the compilation
|
||||
"module": "nodenext", // Specify module code generation
|
||||
"outDir": "./dist",
|
||||
"lib": ["es2020"], // Specify a list of library files to be included in the compilation
|
||||
"types": ["@extism/js-pdk"], // Specify a list of type definition files to be included in the compilation
|
||||
"strict": true, // Enable all strict type-checking options
|
||||
"esModuleInterop": true, // Enables compatibility with Babel-style module imports
|
||||
"moduleResolution": "nodenext",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"skipLibCheck": true, // Skip type checking of declaration files
|
||||
"allowJs": true, // Allow JavaScript files to be compiled
|
||||
"noEmit": true // Do not emit outputs (no .js or .d.ts files)
|
||||
"allowJs": true // Allow JavaScript files to be compiled
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts" // Include all TypeScript files in src directory
|
||||
2
packages/plugin-sdk/.gitignore
vendored
Normal file
2
packages/plugin-sdk/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules
|
||||
11
packages/plugin-sdk/esbuild.js
Normal file
11
packages/plugin-sdk/esbuild.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import esbuild from 'esbuild';
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
outdir: 'dist',
|
||||
bundle: true,
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
format: 'esm',
|
||||
target: ['es2020'],
|
||||
});
|
||||
38
packages/plugin-sdk/package.json
Normal file
38
packages/plugin-sdk/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@immich/plugin-sdk",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./host-functions": {
|
||||
"import": "./dist/host-functions.js",
|
||||
"types": "./dist/host-functions.d.ts"
|
||||
},
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "node esbuild.js && tsc --emitDeclarationOnly && tsc-alias"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@10.30.3",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@types/node": "^24.11.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1"
|
||||
}
|
||||
}
|
||||
33
packages/plugin-sdk/src/enum.ts
Normal file
33
packages/plugin-sdk/src/enum.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = 'AssetCreate',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
export enum WorkflowType {
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetPersonV1 = 'AssetPersonV1',
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
Image = 'IMAGE',
|
||||
Video = 'VIDEO',
|
||||
Audio = 'AUDIO',
|
||||
Other = 'OTHER',
|
||||
}
|
||||
|
||||
export enum AssetStatus {
|
||||
Active = 'active',
|
||||
Trashed = 'trashed',
|
||||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export enum AssetVisibility {
|
||||
Archive = 'archive',
|
||||
Timeline = 'timeline',
|
||||
|
||||
/**
|
||||
* Video part of the LivePhotos and MotionPhotos
|
||||
*/
|
||||
Hidden = 'hidden',
|
||||
Locked = 'locked',
|
||||
}
|
||||
41
packages/plugin-sdk/src/host-functions.ts
Normal file
41
packages/plugin-sdk/src/host-functions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
albumAddAssets(ptr: PTR): I64;
|
||||
}
|
||||
}
|
||||
|
||||
const host = Host.getFunctions();
|
||||
type HostFunctionName = keyof typeof host;
|
||||
|
||||
const call = <T, R>(name: HostFunctionName, authToken: string, args: T) => {
|
||||
const pointer1 = Memory.fromString(JSON.stringify({ authToken, args }));
|
||||
const fn = host[name];
|
||||
const handler = Memory.find(fn(pointer1.offset));
|
||||
|
||||
try {
|
||||
const result = JSON.parse(handler.readString()) as
|
||||
| {
|
||||
success: true;
|
||||
response: R;
|
||||
}
|
||||
| { success: false; status: number; message: string };
|
||||
|
||||
if (result.success) {
|
||||
return result.response;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to call host function "${name}", received ${
|
||||
result.status
|
||||
} - ${JSON.stringify(result.message)}`
|
||||
);
|
||||
} finally {
|
||||
handler.free();
|
||||
pointer1.free();
|
||||
}
|
||||
};
|
||||
|
||||
export const hostFunctions = (authToken: string) => ({
|
||||
albumAddAssets: (albumId: string, assetIds: string[]) =>
|
||||
call('albumAddAssets', authToken, [albumId, { ids: assetIds }]),
|
||||
});
|
||||
4
packages/plugin-sdk/src/index.ts
Normal file
4
packages/plugin-sdk/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from 'src/enum.js';
|
||||
export * from 'src/host-functions.js';
|
||||
export * from 'src/sdk.js';
|
||||
export * from 'src/types.js';
|
||||
47
packages/plugin-sdk/src/sdk.ts
Normal file
47
packages/plugin-sdk/src/sdk.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { WorkflowType } from 'src/enum.js';
|
||||
import { hostFunctions } from 'src/host-functions.js';
|
||||
import type {
|
||||
ConfigValue,
|
||||
WorkflowEventPayload,
|
||||
WorkflowResponse,
|
||||
} from 'src/types.js';
|
||||
|
||||
export const wrapper = <
|
||||
T extends WorkflowType = WorkflowType,
|
||||
TConfig extends ConfigValue = ConfigValue
|
||||
>(
|
||||
fn: (
|
||||
payload: WorkflowEventPayload<T, TConfig> & {
|
||||
functions: ReturnType<typeof hostFunctions>;
|
||||
}
|
||||
) => WorkflowResponse<T> | undefined
|
||||
) => {
|
||||
try {
|
||||
const input = Host.inputString();
|
||||
const event = JSON.parse(input) as WorkflowEventPayload<T, TConfig>;
|
||||
const debug = event.workflow.debug ?? false;
|
||||
|
||||
if (debug) {
|
||||
console.trace(`Event trigger: ${event.trigger}`);
|
||||
console.trace(`Event type: ${event.type}`);
|
||||
console.trace(`Event data: ${JSON.stringify(event.data)}`);
|
||||
console.trace(`Event config: ${JSON.stringify(event.config)}`);
|
||||
}
|
||||
|
||||
const response =
|
||||
fn({ ...event, functions: hostFunctions(event.workflow.authToken) }) ??
|
||||
{};
|
||||
|
||||
if (debug) {
|
||||
console.trace(`Output workflow: ${JSON.stringify(response.workflow)}`);
|
||||
console.trace(`Output changes: ${JSON.stringify(response.changes)}`);
|
||||
console.trace(`Output data: ${JSON.stringify(response.data)}`);
|
||||
}
|
||||
|
||||
const output = JSON.stringify(response);
|
||||
Host.outputString(output);
|
||||
} catch (error: Error | any) {
|
||||
console.error(`Unhandled plugin exception: ${error.message || error}`);
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
};
|
||||
124
packages/plugin-sdk/src/types.ts
Normal file
124
packages/plugin-sdk/src/types.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
} from 'src/enum.js';
|
||||
|
||||
type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type WorkflowEventMap = {
|
||||
[WorkflowType.AssetV1]: AssetV1;
|
||||
[WorkflowType.AssetPersonV1]: AssetPersonV1;
|
||||
};
|
||||
|
||||
export type WorkflowEventData<T extends WorkflowType> = WorkflowEventMap[T];
|
||||
|
||||
export type WorkflowEventPayload<
|
||||
T extends WorkflowType = WorkflowType,
|
||||
TConfig = WorkflowStepConfig
|
||||
> = {
|
||||
trigger: WorkflowTrigger;
|
||||
type: T;
|
||||
data: WorkflowEventData<T>;
|
||||
config: TConfig;
|
||||
workflow: {
|
||||
id: string;
|
||||
authToken: string;
|
||||
stepId: string;
|
||||
debug?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowResponse<T extends WorkflowType = WorkflowType> = {
|
||||
workflow?: {
|
||||
/** stop the workflow */
|
||||
continue?: boolean;
|
||||
};
|
||||
changes?: DeepPartial<WorkflowEventData<T>>;
|
||||
/** data to be passed to the next workflow step */
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type WorkflowStepConfig = {
|
||||
[key: string]: ConfigValue;
|
||||
};
|
||||
|
||||
export type ConfigValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| ConfigValue[]
|
||||
| { [key: string]: ConfigValue };
|
||||
|
||||
export type AssetV1 = {
|
||||
asset: {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
type: AssetType;
|
||||
originalPath: string;
|
||||
fileCreatedAt: Date;
|
||||
fileModifiedAt: Date;
|
||||
isFavorite: boolean;
|
||||
checksum: Buffer; // sha1 checksum
|
||||
livePhotoVideoId: string | null;
|
||||
updatedAt: Date;
|
||||
createdAt: Date;
|
||||
originalFileName: string;
|
||||
isOffline: boolean;
|
||||
libraryId: string | null;
|
||||
isExternal: boolean;
|
||||
deletedAt: Date | null;
|
||||
localDateTime: Date;
|
||||
stackId: string | null;
|
||||
duplicateId: string | null;
|
||||
status: AssetStatus;
|
||||
visibility: AssetVisibility;
|
||||
isEdited: boolean;
|
||||
exifInfo: {
|
||||
make: string | null;
|
||||
model: string | null;
|
||||
exifImageWidth: number | null;
|
||||
exifImageHeight: number | null;
|
||||
fileSizeInByte: number | null;
|
||||
orientation: string | null;
|
||||
dateTimeOriginal: Date | null;
|
||||
modifyDate: Date | null;
|
||||
lensModel: string | null;
|
||||
fNumber: number | null;
|
||||
focalLength: number | null;
|
||||
iso: number | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
country: string | null;
|
||||
description: string | null;
|
||||
fps: number | null;
|
||||
exposureTime: string | null;
|
||||
livePhotoCID: string | null;
|
||||
timeZone: string | null;
|
||||
projectionType: string | null;
|
||||
profileDescription: string | null;
|
||||
colorspace: string | null;
|
||||
bitsPerSample: number | null;
|
||||
autoStackId: string | null;
|
||||
rating: number | null;
|
||||
tags: string[] | null;
|
||||
updatedAt: Date | null;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type AssetPersonV1 = AssetV1 & {
|
||||
person: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
26
packages/plugin-sdk/tsconfig.json
Normal file
26
packages/plugin-sdk/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"strict": true,
|
||||
"removeComments": true,
|
||||
"lib": ["esnext"],
|
||||
"outDir": "./dist",
|
||||
"types": ["node", "@extism/js-pdk"],
|
||||
"sourceMap": false,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"src/*": ["./src/*"]
|
||||
},
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
2
plugins/.gitignore
vendored
2
plugins/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,26 +0,0 @@
|
||||
Copyright 2024, The Extism Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,12 +0,0 @@
|
||||
const esbuild = require('esbuild');
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
outdir: 'dist',
|
||||
bundle: true,
|
||||
sourcemap: true,
|
||||
minify: false, // might want to use true for production build
|
||||
format: 'cjs', // needs to be CJS for now
|
||||
target: ['es2020'] // don't go over es2020 because quickjs doesn't support it
|
||||
})
|
||||
@@ -1,159 +0,0 @@
|
||||
{
|
||||
"name": "immich-core",
|
||||
"version": "2.0.1",
|
||||
"title": "Immich Core",
|
||||
"description": "Core workflow capabilities for Immich",
|
||||
"author": "Immich Team",
|
||||
"wasm": {
|
||||
"path": "dist/plugin.wasm"
|
||||
},
|
||||
"filters": [
|
||||
{
|
||||
"methodName": "filterFileName",
|
||||
"title": "Filter by filename",
|
||||
"description": "Filter assets by filename pattern using text matching or regular expressions",
|
||||
"supportedContexts": [
|
||||
"asset"
|
||||
],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"title": "Filename pattern",
|
||||
"description": "Text or regex pattern to match against filename"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": [
|
||||
"contains",
|
||||
"regex",
|
||||
"exact"
|
||||
],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Whether matching should be case-sensitive"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"methodName": "filterFileType",
|
||||
"title": "Filter by file type",
|
||||
"description": "Filter assets by file type",
|
||||
"supportedContexts": [
|
||||
"asset"
|
||||
],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileTypes": {
|
||||
"type": "array",
|
||||
"title": "File types",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"image",
|
||||
"video"
|
||||
]
|
||||
},
|
||||
"description": "Allowed file types"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fileTypes"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"methodName": "filterPerson",
|
||||
"title": "Filter by person",
|
||||
"description": "Filter by detected person",
|
||||
"supportedContexts": [
|
||||
"person"
|
||||
],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"personIds": {
|
||||
"type": "array",
|
||||
"title": "Person IDs",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of person to match",
|
||||
"subType": "people-picker"
|
||||
},
|
||||
"matchAny": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Match any name (true) or require all names (false)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"personIds"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"methodName": "actionArchive",
|
||||
"title": "Archive",
|
||||
"description": "Move the asset to archive",
|
||||
"supportedContexts": [
|
||||
"asset"
|
||||
],
|
||||
"schema": {}
|
||||
},
|
||||
{
|
||||
"methodName": "actionFavorite",
|
||||
"title": "Favorite",
|
||||
"description": "Mark the asset as favorite or unfavorite",
|
||||
"supportedContexts": [
|
||||
"asset"
|
||||
],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"favorite": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Set favorite (true) or unfavorite (false)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"methodName": "actionAddToAlbum",
|
||||
"title": "Add to Album",
|
||||
"description": "Add the item to a specified album",
|
||||
"supportedContexts": [
|
||||
"asset",
|
||||
"person"
|
||||
],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"albumId": {
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"subType": "album-picker"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"albumId"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
533
plugins/package-lock.json
generated
533
plugins/package-lock.json
generated
@@ -1,533 +0,0 @@
|
||||
{
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.27.0",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@extism/js-pdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.1.1.tgz",
|
||||
"integrity": "sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==",
|
||||
"dev": true,
|
||||
"license": "BSD-Clause-3",
|
||||
"dependencies": {
|
||||
"urlpattern-polyfill": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/urlpattern-polyfill": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz",
|
||||
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
plugins/src/index.d.ts
vendored
12
plugins/src/index.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
declare module 'main' {
|
||||
export function filterFileName(): I32;
|
||||
export function actionAddToAlbum(): I32;
|
||||
export function actionArchive(): I32;
|
||||
}
|
||||
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
updateAsset(ptr: PTR): I32;
|
||||
addAssetToAlbum(ptr: PTR): I32;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
const { updateAsset, addAssetToAlbum } = Host.getFunctions();
|
||||
|
||||
function parseInput() {
|
||||
return JSON.parse(Host.inputString());
|
||||
}
|
||||
|
||||
function returnOutput(output: any) {
|
||||
Host.outputString(JSON.stringify(output));
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function filterFileName() {
|
||||
const input = parseInput();
|
||||
const { data, config } = input;
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
|
||||
const fileName = data.asset.originalFileName || data.asset.fileName || '';
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
let passed = false;
|
||||
|
||||
if (matchType === 'exact') {
|
||||
passed = searchName === searchPattern;
|
||||
} else if (matchType === 'regex') {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
passed = regex.test(fileName);
|
||||
} else {
|
||||
// contains
|
||||
passed = searchName.includes(searchPattern);
|
||||
}
|
||||
|
||||
return returnOutput({ passed });
|
||||
}
|
||||
|
||||
export function actionAddToAlbum() {
|
||||
const input = parseInput();
|
||||
const { authToken, config, data } = input;
|
||||
const { albumId } = config;
|
||||
|
||||
const ptr = Memory.fromString(
|
||||
JSON.stringify({
|
||||
authToken,
|
||||
assetId: data.asset.id,
|
||||
albumId: albumId,
|
||||
})
|
||||
);
|
||||
|
||||
addAssetToAlbum(ptr.offset);
|
||||
ptr.free();
|
||||
|
||||
return returnOutput({ success: true });
|
||||
}
|
||||
|
||||
export function actionArchive() {
|
||||
const input = parseInput();
|
||||
const { authToken, data } = input;
|
||||
const ptr = Memory.fromString(
|
||||
JSON.stringify({
|
||||
authToken,
|
||||
id: data.asset.id,
|
||||
visibility: 'archive',
|
||||
})
|
||||
);
|
||||
|
||||
updateAsset(ptr.offset);
|
||||
ptr.free();
|
||||
|
||||
return returnOutput({ success: true });
|
||||
}
|
||||
67
pnpm-lock.yaml
generated
67
pnpm-lock.yaml
generated
@@ -329,11 +329,14 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
|
||||
plugins:
|
||||
packages/plugin-core:
|
||||
devDependencies:
|
||||
'@extism/js-pdk':
|
||||
specifier: ^1.0.1
|
||||
version: 1.1.1
|
||||
'@immich/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../plugin-sdk
|
||||
esbuild:
|
||||
specifier: ^0.27.0
|
||||
version: 0.27.3
|
||||
@@ -341,11 +344,32 @@ importers:
|
||||
specifier: ^5.3.2
|
||||
version: 5.9.3
|
||||
|
||||
packages/plugin-sdk:
|
||||
devDependencies:
|
||||
'@extism/js-pdk':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@types/node':
|
||||
specifier: ^24.11.0
|
||||
version: 24.11.0
|
||||
esbuild:
|
||||
specifier: ^0.27.3
|
||||
version: 0.27.3
|
||||
tsc-alias:
|
||||
specifier: ^1.8.16
|
||||
version: 1.8.16
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
'@extism/extism':
|
||||
specifier: 2.0.0-rc13
|
||||
version: 2.0.0-rc13
|
||||
'@immich/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/plugin-sdk
|
||||
'@immich/sql-tools':
|
||||
specifier: ^0.3.2
|
||||
version: 0.3.2
|
||||
@@ -6121,6 +6145,10 @@ packages:
|
||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
commander@9.5.0:
|
||||
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||
engines: {node: ^12.20.0 || >=14}
|
||||
|
||||
comment-json@4.4.1:
|
||||
resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -9136,6 +9164,10 @@ packages:
|
||||
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
|
||||
engines: {node: ^18.17.0 || >=20.5.0}
|
||||
|
||||
mylas@2.1.14:
|
||||
resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
@@ -9656,6 +9688,10 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
plimit-lit@1.6.1:
|
||||
resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -10288,6 +10324,10 @@ packages:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-lit@1.5.2:
|
||||
resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -11510,6 +11550,11 @@ packages:
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
tsc-alias@1.8.16:
|
||||
resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==}
|
||||
engines: {node: '>=16.20.2'}
|
||||
hasBin: true
|
||||
|
||||
tsconfck@3.1.6:
|
||||
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -18363,6 +18408,8 @@ snapshots:
|
||||
|
||||
commander@8.3.0: {}
|
||||
|
||||
commander@9.5.0: {}
|
||||
|
||||
comment-json@4.4.1:
|
||||
dependencies:
|
||||
array-timsort: 1.0.3
|
||||
@@ -22075,6 +22122,8 @@ snapshots:
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
mylas@2.1.14: {}
|
||||
|
||||
mz@2.7.0:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
@@ -22618,6 +22667,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
plimit-lit@1.6.1:
|
||||
dependencies:
|
||||
queue-lit: 1.5.2
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
pmtiles@3.2.1:
|
||||
@@ -23284,6 +23337,8 @@ snapshots:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
queue-lit@1.5.2: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-lru@5.1.1: {}
|
||||
@@ -24876,6 +24931,16 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsc-alias@1.8.16:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
commander: 9.5.0
|
||||
get-tsconfig: 4.13.0
|
||||
globby: 11.1.0
|
||||
mylas: 2.1.14
|
||||
normalize-path: 3.0.0
|
||||
plimit-lit: 1.6.1
|
||||
|
||||
tsconfck@3.1.6(typescript@5.9.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
@@ -9,6 +9,7 @@ packages:
|
||||
- plugins
|
||||
- web
|
||||
- .github
|
||||
- packages/*
|
||||
ignoredBuiltDependencies:
|
||||
- '@nestjs/core'
|
||||
- '@parcel/watcher'
|
||||
|
||||
@@ -13,12 +13,13 @@ FROM builder AS server
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./server ./server/
|
||||
COPY ./packages/plugin-sdk ./packages/plugin-sdk/
|
||||
RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --filter @immich/plugin-sdk --frozen-lockfile build && \
|
||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
||||
|
||||
FROM builder AS web
|
||||
@@ -55,21 +56,21 @@ ARG TARGETPLATFORM
|
||||
COPY --from=ghcr.io/jdx/mise:2026.1.1@sha256:a55c391f7582f34c58bce1a85090cd526596402ba77fc32b06c49b8404ef9c14 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./plugins/mise.toml ./plugins/
|
||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
||||
COPY ./packages/plugin-core/mise.toml ./packages/plugin-core/
|
||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/packages/plugin-core/mise.toml
|
||||
ENV MISE_DATA_DIR=/buildcache/mise
|
||||
RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise install --cd plugins
|
||||
mise install --cd packages/plugin-core
|
||||
|
||||
COPY ./plugins ./plugins/
|
||||
COPY ./packages ./packages/
|
||||
# Build plugins
|
||||
RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
||||
RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
cd plugins && mise run build
|
||||
cd packages/plugin-core && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202603031112@sha256:bb8c8645ee61977140121e56ba09db7ae656a7506f9a6af1be8461b4d81fdf03
|
||||
|
||||
@@ -81,8 +82,8 @@ ENV NODE_ENV=production \
|
||||
COPY --from=server /output/server-pruned ./server
|
||||
COPY --from=web /usr/src/app/web/build /build/www
|
||||
COPY --from=cli /output/cli-pruned ./cli
|
||||
COPY --from=plugins /usr/src/app/plugins/dist /build/corePlugin/dist
|
||||
COPY --from=plugins /usr/src/app/plugins/manifest.json /build/corePlugin/manifest.json
|
||||
COPY --from=plugins /usr/src/app/packages/plugin-core/dist /build/corePlugin/dist
|
||||
COPY --from=plugins /usr/src/app/packages/plugin-core/manifest.json /build/corePlugin/manifest.json
|
||||
RUN ln -s ../../cli/bin/immich server/bin/immich
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@extism/extism": "2.0.0-rc13",
|
||||
"@immich/plugin-sdk": "workspace:*",
|
||||
"@immich/sql-tools": "^0.3.2",
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
|
||||
56
server/src/controllers/plugin.controller.spec.ts
Normal file
56
server/src/controllers/plugin.controller.spec.ts
Normal file
@@ -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.badRequest([expect.stringContaining('must be a 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.badRequest([expect.stringContaining('must be a 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);
|
||||
|
||||
113
server/src/controllers/workflow.controller.spec.ts
Normal file
113
server/src/controllers/workflow.controller.spec.ts
Normal file
@@ -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.badRequest(
|
||||
expect.arrayContaining([expect.stringContaining('trigger must be one of the following values')]),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
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.badRequest(expect.arrayContaining([expect.stringContaining('enabled must be a boolean')])),
|
||||
);
|
||||
});
|
||||
|
||||
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.badRequest([expect.stringContaining('must be a 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.badRequest([expect.stringContaining('must be a 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.badRequest([expect.stringContaining('must be a UUID')]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,14 @@
|
||||
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,
|
||||
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 +24,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 +35,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,7 +57,7 @@ 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);
|
||||
@@ -52,7 +69,7 @@ export class WorkflowController {
|
||||
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 +85,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);
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
Permission,
|
||||
PluginContext,
|
||||
PluginTriggerType,
|
||||
SharedLinkType,
|
||||
SourceType,
|
||||
UserAvatarColor,
|
||||
@@ -16,10 +14,8 @@ import {
|
||||
} from 'src/enum';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { PluginActionTable, PluginFilterTable, PluginTable } from 'src/schema/tables/plugin.table';
|
||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
import { PluginTable } from 'src/schema/tables/plugin.table';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import type { ActionConfig, FilterConfig, JSONSchema } from 'src/types/plugin-schema.types';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
@@ -278,43 +274,6 @@ export type AssetFace = {
|
||||
|
||||
export type Plugin = Selectable<PluginTable>;
|
||||
|
||||
export type PluginFilter = Selectable<PluginFilterTable> & {
|
||||
methodName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
supportedContexts: PluginContext[];
|
||||
schema: JSONSchema | null;
|
||||
};
|
||||
|
||||
export type PluginAction = Selectable<PluginActionTable> & {
|
||||
methodName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
supportedContexts: PluginContext[];
|
||||
schema: JSONSchema | null;
|
||||
};
|
||||
|
||||
export type Workflow = Selectable<WorkflowTable> & {
|
||||
triggerType: PluginTriggerType;
|
||||
name: string | null;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowFilter = Selectable<WorkflowFilterTable> & {
|
||||
workflowId: string;
|
||||
pluginFilterId: string;
|
||||
filterConfig: FilterConfig | null;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type WorkflowAction = Selectable<WorkflowActionTable> & {
|
||||
workflowId: string;
|
||||
pluginActionId: string;
|
||||
actionConfig: ActionConfig | null;
|
||||
order: number;
|
||||
};
|
||||
|
||||
const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
|
||||
const userWithPrefixColumns = [
|
||||
'user2.id',
|
||||
@@ -345,6 +304,32 @@ export const columns = {
|
||||
'asset.width',
|
||||
'asset.height',
|
||||
],
|
||||
workflowAssetV1: [
|
||||
'asset.id',
|
||||
'asset.ownerId',
|
||||
'asset.stackId',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.libraryId',
|
||||
'asset.duplicateId',
|
||||
'asset.createdAt',
|
||||
'asset.updatedAt',
|
||||
'asset.deletedAt',
|
||||
'asset.fileCreatedAt',
|
||||
'asset.fileModifiedAt',
|
||||
'asset.localDateTime',
|
||||
'asset.type',
|
||||
'asset.status',
|
||||
'asset.visibility',
|
||||
'asset.duration',
|
||||
'asset.checksum',
|
||||
'asset.originalPath',
|
||||
'asset.originalFileName',
|
||||
'asset.isOffline',
|
||||
'asset.isFavorite',
|
||||
'asset.isExternal',
|
||||
'asset.isEdited',
|
||||
'asset.isFavorite',
|
||||
],
|
||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'],
|
||||
assetFilesForThumbnail: [
|
||||
'asset_file.id',
|
||||
@@ -476,17 +461,6 @@ export const columns = {
|
||||
'asset_exif.tags',
|
||||
'asset_exif.timeZone',
|
||||
],
|
||||
plugin: [
|
||||
'plugin.id as id',
|
||||
'plugin.name as name',
|
||||
'plugin.title as title',
|
||||
'plugin.description as description',
|
||||
'plugin.author as author',
|
||||
'plugin.version as version',
|
||||
'plugin.wasmPath as wasmPath',
|
||||
'plugin.createdAt as createdAt',
|
||||
'plugin.updatedAt as updatedAt',
|
||||
],
|
||||
} as const;
|
||||
|
||||
export type LockableProperty = (typeof lockableProperties)[number];
|
||||
|
||||
@@ -210,6 +210,10 @@ export class HistoryBuilder {
|
||||
private hasDeprecated = false;
|
||||
private items: HistoryEntry[] = [];
|
||||
|
||||
static v3() {
|
||||
return new HistoryBuilder().added('v3.0.0');
|
||||
}
|
||||
|
||||
added(version: string, description?: string) {
|
||||
return this.push({ version, state: 'Added', description });
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
@@ -12,66 +11,31 @@ import {
|
||||
Matches,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { PluginContext } from 'src/enum';
|
||||
import { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
import { WorkflowType } from 'src/enum';
|
||||
import { JSONSchema } from 'src/types';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
class PluginManifestWasmDto {
|
||||
@ApiProperty({ description: 'WASM file path' })
|
||||
class PluginManifestMethodDto {
|
||||
@ApiProperty({ description: 'Method name' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
path!: string;
|
||||
}
|
||||
name!: string;
|
||||
|
||||
class PluginManifestFilterDto {
|
||||
@ApiProperty({ description: 'Filter method name' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
methodName!: string;
|
||||
|
||||
@ApiProperty({ description: 'Filter title' })
|
||||
@ApiProperty({ description: 'Method title' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ description: 'Filter description' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true })
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsEnum(PluginContext, { each: true })
|
||||
supportedContexts!: PluginContext[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter schema' })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
schema?: JSONSchema;
|
||||
}
|
||||
|
||||
class PluginManifestActionDto {
|
||||
@ApiProperty({ description: 'Action method name' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
methodName!: string;
|
||||
|
||||
@ApiProperty({ description: 'Action title' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ description: 'Action description' })
|
||||
@ApiProperty({ description: 'Method description' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@ArrayMinSize(1)
|
||||
@ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true, description: 'Supported contexts' })
|
||||
supportedContexts!: PluginContext[];
|
||||
@ValidateEnum({ enum: WorkflowType, name: 'WorkflowType', each: true, description: 'Workflow type' })
|
||||
types!: WorkflowType[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Action schema' })
|
||||
@ApiPropertyOptional({ description: 'Method schema' })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
schema?: JSONSchema;
|
||||
@@ -102,27 +66,20 @@ export class PluginManifestDto {
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ description: 'WASM file path' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
wasmPath!: string;
|
||||
|
||||
@ApiProperty({ description: 'Plugin author' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
author!: string;
|
||||
|
||||
@ApiProperty({ description: 'WASM configuration' })
|
||||
@ValidateNested()
|
||||
@Type(() => PluginManifestWasmDto)
|
||||
wasm!: PluginManifestWasmDto;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Plugin filters' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PluginManifestFilterDto)
|
||||
@Type(() => PluginManifestMethodDto)
|
||||
@IsOptional()
|
||||
filters?: PluginManifestFilterDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Plugin actions' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PluginManifestActionDto)
|
||||
@IsOptional()
|
||||
actions?: PluginManifestActionDto[];
|
||||
methods!: PluginManifestMethodDto[];
|
||||
}
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { PluginAction, PluginFilter } from 'src/database';
|
||||
import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum';
|
||||
import type { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { JSONSchema } from 'src/types';
|
||||
import { asMethodString } from 'src/utils/workflow';
|
||||
import { ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class PluginTriggerResponseDto {
|
||||
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' })
|
||||
type!: PluginTriggerType;
|
||||
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' })
|
||||
contextType!: PluginContextType;
|
||||
export class PluginSearchDto {
|
||||
@ValidateUUID({ optional: true, description: 'Plugin ID' })
|
||||
id?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Whether the plugin is enabled' })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
name?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
version?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
title?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class PluginResponseDto {
|
||||
@@ -29,45 +42,56 @@ export class PluginResponseDto {
|
||||
createdAt!: string;
|
||||
@ApiProperty({ description: 'Last update date' })
|
||||
updatedAt!: string;
|
||||
@ApiProperty({ description: 'Plugin filters' })
|
||||
filters!: PluginFilterResponseDto[];
|
||||
@ApiProperty({ description: 'Plugin actions' })
|
||||
actions!: PluginActionResponseDto[];
|
||||
@ApiProperty({ description: 'Plugin methods' })
|
||||
methods!: PluginMethodResponseDto[];
|
||||
}
|
||||
|
||||
export class PluginFilterResponseDto {
|
||||
@ApiProperty({ description: 'Filter ID' })
|
||||
id!: string;
|
||||
@ApiProperty({ description: 'Plugin ID' })
|
||||
pluginId!: string;
|
||||
@ApiProperty({ description: 'Method name' })
|
||||
methodName!: string;
|
||||
@ApiProperty({ description: 'Filter title' })
|
||||
title!: string;
|
||||
@ApiProperty({ description: 'Filter description' })
|
||||
description!: string;
|
||||
export class PluginMethodSearchDto {
|
||||
@ValidateUUID({ optional: true, description: 'Plugin method ID' })
|
||||
id?: string;
|
||||
|
||||
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' })
|
||||
supportedContexts!: PluginContextType[];
|
||||
@ApiProperty({ description: 'Filter schema' })
|
||||
schema!: JSONSchema | null;
|
||||
@ValidateBoolean({ optional: true, description: 'Whether the plugin method is enabled' })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
name?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
title?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
description?: string;
|
||||
|
||||
@ValidateEnum({ optional: true, enum: WorkflowType, name: 'WorkflowType' })
|
||||
type?: WorkflowType;
|
||||
|
||||
@ValidateEnum({ optional: true, enum: WorkflowTrigger, name: 'WorkflowTrigger' })
|
||||
trigger?: WorkflowTrigger;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
pluginName?: string;
|
||||
|
||||
@ValidateString({ optional: true })
|
||||
pluginVersion?: string;
|
||||
}
|
||||
|
||||
export class PluginActionResponseDto {
|
||||
@ApiProperty({ description: 'Action ID' })
|
||||
id!: string;
|
||||
@ApiProperty({ description: 'Plugin ID' })
|
||||
pluginId!: string;
|
||||
@ApiProperty({ description: 'Method name' })
|
||||
methodName!: string;
|
||||
@ApiProperty({ description: 'Action title' })
|
||||
export class PluginMethodResponseDto {
|
||||
@ApiProperty({ description: 'Key' })
|
||||
key!: string;
|
||||
|
||||
@ApiProperty({ description: 'Name' })
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ description: 'Title' })
|
||||
title!: string;
|
||||
@ApiProperty({ description: 'Action description' })
|
||||
|
||||
@ApiProperty({ description: 'Description' })
|
||||
description!: string;
|
||||
|
||||
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' })
|
||||
supportedContexts!: PluginContextType[];
|
||||
@ApiProperty({ description: 'Action schema' })
|
||||
@ValidateEnum({ name: 'WorkflowType', enum: WorkflowType, each: true, description: 'Workflow types' })
|
||||
types!: WorkflowType[];
|
||||
|
||||
@ApiProperty({ description: 'Schema' })
|
||||
schema!: JSONSchema | null;
|
||||
}
|
||||
|
||||
@@ -78,21 +102,28 @@ export class PluginInstallDto {
|
||||
manifestPath!: string;
|
||||
}
|
||||
|
||||
export type MapPlugin = {
|
||||
type Plugin = {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
version: string;
|
||||
wasmPath: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
filters: PluginFilter[];
|
||||
actions: PluginAction[];
|
||||
methods: PluginMethod[];
|
||||
};
|
||||
|
||||
export function mapPlugin(plugin: MapPlugin): PluginResponseDto {
|
||||
type PluginMethod = {
|
||||
pluginName: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
types: WorkflowType[];
|
||||
schema: JSONSchema | null;
|
||||
};
|
||||
|
||||
export function mapPlugin(plugin: Plugin): PluginResponseDto {
|
||||
return {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
@@ -102,7 +133,17 @@ export function mapPlugin(plugin: MapPlugin): PluginResponseDto {
|
||||
version: plugin.version,
|
||||
createdAt: plugin.createdAt.toISOString(),
|
||||
updatedAt: plugin.updatedAt.toISOString(),
|
||||
filters: plugin.filters,
|
||||
actions: plugin.actions,
|
||||
methods: plugin.methods.map((method) => mapMethod(method)),
|
||||
};
|
||||
}
|
||||
|
||||
export const mapMethod = (method: PluginMethod): PluginMethodResponseDto => {
|
||||
return {
|
||||
key: asMethodString({ pluginName: method.pluginName, methodName: method.name }),
|
||||
name: method.name,
|
||||
title: method.title,
|
||||
description: method.description,
|
||||
types: method.types,
|
||||
schema: method.schema,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,160 +1,149 @@
|
||||
import type { WorkflowStepConfig } from '@immich/plugin-sdk';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNotEmpty, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { WorkflowAction, WorkflowFilter } from 'src/database';
|
||||
import { PluginTriggerType } from 'src/enum';
|
||||
import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types';
|
||||
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
import { IsObject, ValidateNested } from 'class-validator';
|
||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class WorkflowFilterItemDto {
|
||||
@ApiProperty({ description: 'Plugin filter ID' })
|
||||
@IsUUID()
|
||||
pluginFilterId!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Filter configuration' })
|
||||
@IsObject()
|
||||
@Optional()
|
||||
filterConfig?: FilterConfig;
|
||||
export class WorkflowTriggerResponseDto {
|
||||
@ValidateEnum({ enum: WorkflowTrigger, name: 'WorkflowTrigger', description: 'Trigger type' })
|
||||
trigger!: WorkflowTrigger;
|
||||
@ValidateEnum({ enum: WorkflowType, name: 'WorkflowType', description: 'Workflow types', each: true })
|
||||
types!: WorkflowType[];
|
||||
}
|
||||
|
||||
export class WorkflowActionItemDto {
|
||||
@ApiProperty({ description: 'Plugin action ID' })
|
||||
@IsUUID()
|
||||
pluginActionId!: string;
|
||||
export class WorkflowSearchDto {
|
||||
@ValidateUUID({ optional: true, description: 'Workflow ID' })
|
||||
id?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Action configuration' })
|
||||
@IsObject()
|
||||
@Optional()
|
||||
actionConfig?: ActionConfig;
|
||||
}
|
||||
@ValidateEnum({
|
||||
optional: true,
|
||||
enum: WorkflowTrigger,
|
||||
name: 'WorkflowTrigger',
|
||||
description: 'Workflow trigger type',
|
||||
})
|
||||
trigger?: WorkflowTrigger;
|
||||
|
||||
export class WorkflowCreateDto {
|
||||
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' })
|
||||
triggerType!: PluginTriggerType;
|
||||
@ValidateString({ optional: true, description: 'Workflow name' })
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: 'Workflow name' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow description' })
|
||||
@IsString()
|
||||
@Optional()
|
||||
@ValidateString({ optional: true, description: 'Workflow description' })
|
||||
description?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Workflow enabled' })
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Workflow filters' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowFilterItemDto)
|
||||
filters!: WorkflowFilterItemDto[];
|
||||
|
||||
@ApiProperty({ description: 'Workflow actions' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowActionItemDto)
|
||||
actions!: WorkflowActionItemDto[];
|
||||
}
|
||||
|
||||
export class WorkflowUpdateDto {
|
||||
class WorkflowBase {
|
||||
@ValidateString({ optional: true, nullable: true, description: 'Workflow name' })
|
||||
name?: string | null;
|
||||
|
||||
@ValidateString({ optional: true, nullable: true, description: 'Workflow description' })
|
||||
description?: string | null;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Workflow enabled' })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowStepDto)
|
||||
@Optional()
|
||||
steps?: WorkflowStepDto[];
|
||||
}
|
||||
|
||||
export class WorkflowCreateDto extends WorkflowBase {
|
||||
@ValidateEnum({ enum: WorkflowTrigger, name: 'WorkflowTrigger', description: 'Workflow trigger type' })
|
||||
trigger!: WorkflowTrigger;
|
||||
}
|
||||
|
||||
export class WorkflowUpdateDto extends WorkflowBase {
|
||||
@ValidateEnum({
|
||||
enum: PluginTriggerType,
|
||||
name: 'PluginTriggerType',
|
||||
enum: WorkflowTrigger,
|
||||
name: 'WorkflowTrigger',
|
||||
optional: true,
|
||||
description: 'Workflow trigger type',
|
||||
})
|
||||
triggerType?: PluginTriggerType;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow name' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow description' })
|
||||
@IsString()
|
||||
@Optional()
|
||||
description?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Workflow enabled' })
|
||||
enabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow filters' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowFilterItemDto)
|
||||
@Optional()
|
||||
filters?: WorkflowFilterItemDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Workflow actions' })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowActionItemDto)
|
||||
@Optional()
|
||||
actions?: WorkflowActionItemDto[];
|
||||
trigger?: WorkflowTrigger;
|
||||
}
|
||||
|
||||
export class WorkflowResponseDto {
|
||||
@ApiProperty({ description: 'Workflow ID' })
|
||||
id!: string;
|
||||
@ApiProperty({ description: 'Owner user ID' })
|
||||
ownerId!: string;
|
||||
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' })
|
||||
triggerType!: PluginTriggerType;
|
||||
|
||||
@ValidateEnum({ enum: WorkflowTrigger, name: 'WorkflowTrigger', description: 'Workflow trigger type' })
|
||||
trigger!: WorkflowTrigger;
|
||||
|
||||
@ApiProperty({ description: 'Workflow name' })
|
||||
name!: string | null;
|
||||
|
||||
@ApiProperty({ description: 'Workflow description' })
|
||||
description!: string;
|
||||
description!: string | null;
|
||||
|
||||
@ApiProperty({ description: 'Creation date' })
|
||||
createdAt!: string;
|
||||
|
||||
@ApiProperty({ description: 'Update date' })
|
||||
updatedAt!: string;
|
||||
|
||||
@ApiProperty({ description: 'Workflow enabled' })
|
||||
enabled!: boolean;
|
||||
@ApiProperty({ description: 'Workflow filters' })
|
||||
filters!: WorkflowFilterResponseDto[];
|
||||
@ApiProperty({ description: 'Workflow actions' })
|
||||
actions!: WorkflowActionResponseDto[];
|
||||
|
||||
@ApiProperty({ description: 'Workflow steps' })
|
||||
steps!: WorkflowStepResponseDto[];
|
||||
}
|
||||
|
||||
export class WorkflowFilterResponseDto {
|
||||
@ApiProperty({ description: 'Filter ID' })
|
||||
id!: string;
|
||||
@ApiProperty({ description: 'Workflow ID' })
|
||||
workflowId!: string;
|
||||
@ApiProperty({ description: 'Plugin filter ID' })
|
||||
pluginFilterId!: string;
|
||||
@ApiProperty({ description: 'Filter configuration' })
|
||||
filterConfig!: FilterConfig | null;
|
||||
@ApiProperty({ description: 'Filter order', type: 'number' })
|
||||
order!: number;
|
||||
export class WorkflowStepDto {
|
||||
@ValidateString({ description: 'Step plugin method' })
|
||||
method!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Step configuration' })
|
||||
@IsObject()
|
||||
@Optional({ nullable: true })
|
||||
config?: WorkflowStepConfig | null;
|
||||
|
||||
@ValidateBoolean({ optional: true, description: 'Step is enabled' })
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export class WorkflowActionResponseDto {
|
||||
@ApiProperty({ description: 'Action ID' })
|
||||
id!: string;
|
||||
@ApiProperty({ description: 'Workflow ID' })
|
||||
workflowId!: string;
|
||||
@ApiProperty({ description: 'Plugin action ID' })
|
||||
pluginActionId!: string;
|
||||
@ApiProperty({ description: 'Action configuration' })
|
||||
actionConfig!: ActionConfig | null;
|
||||
@ApiProperty({ description: 'Action order', type: 'number' })
|
||||
order!: number;
|
||||
export class WorkflowStepResponseDto {
|
||||
@ApiProperty({ description: 'Step plugin method' })
|
||||
method!: string;
|
||||
|
||||
@ApiProperty({ description: 'Step configuration' })
|
||||
config!: WorkflowStepConfig | null;
|
||||
|
||||
@ValidateBoolean({ description: 'Step is enabled' })
|
||||
enabled!: boolean;
|
||||
}
|
||||
|
||||
export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterResponseDto {
|
||||
export type Workflow = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
trigger: WorkflowTrigger;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
enabled: boolean;
|
||||
};
|
||||
export type WorkflowStep = {
|
||||
enabled: boolean;
|
||||
methodName: string;
|
||||
config: WorkflowStepConfig | null;
|
||||
pluginName: string;
|
||||
};
|
||||
|
||||
export const mapWorkflow = (workflow: Workflow & { steps: WorkflowStep[] }): WorkflowResponseDto => {
|
||||
return {
|
||||
id: filter.id,
|
||||
workflowId: filter.workflowId,
|
||||
pluginFilterId: filter.pluginFilterId,
|
||||
filterConfig: filter.filterConfig,
|
||||
order: filter.order,
|
||||
id: workflow.id,
|
||||
trigger: workflow.trigger,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
createdAt: workflow.createdAt.toISOString(),
|
||||
updatedAt: workflow.updatedAt.toISOString(),
|
||||
enabled: workflow.enabled,
|
||||
steps: workflow.steps.map((step) => ({
|
||||
method: `${step.pluginName}#${step.methodName}`,
|
||||
config: step.config,
|
||||
enabled: step.enabled,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapWorkflowAction(action: WorkflowAction): WorkflowActionResponseDto {
|
||||
return {
|
||||
id: action.id,
|
||||
workflowId: action.workflowId,
|
||||
pluginActionId: action.pluginActionId,
|
||||
actionConfig: action.actionConfig,
|
||||
order: action.order,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -544,8 +544,11 @@ export enum BootstrapEventPriority {
|
||||
StorageService = -195,
|
||||
// Other services may need to queue jobs on bootstrap.
|
||||
JobService = -190,
|
||||
// Initialise config after other bootstrap services, stop other services from using config on bootstrap
|
||||
// Initialize config after other bootstrap services, stop other services from using config on bootstrap
|
||||
SystemConfig = 100,
|
||||
PluginSync = 190,
|
||||
// Load plugins into memory after sync
|
||||
PluginLoad = 200,
|
||||
}
|
||||
|
||||
export enum QueueName {
|
||||
@@ -655,7 +658,7 @@ export enum JobName {
|
||||
Ocr = 'Ocr',
|
||||
|
||||
// Workflow
|
||||
WorkflowRun = 'WorkflowRun',
|
||||
WorkflowAssetCreate = 'WorkflowAssetCreate',
|
||||
}
|
||||
|
||||
export enum QueueCommand {
|
||||
@@ -694,6 +697,7 @@ export enum DatabaseLock {
|
||||
CLIPDimSize = 512,
|
||||
Library = 1337,
|
||||
NightlyJobs = 600,
|
||||
PluginImport = 666,
|
||||
MediaLocation = 700,
|
||||
GetSystemConfig = 69,
|
||||
BackupDatabase = 42,
|
||||
@@ -882,13 +886,12 @@ export enum ApiTag {
|
||||
Workflows = 'Workflows',
|
||||
}
|
||||
|
||||
export enum PluginContext {
|
||||
Asset = 'asset',
|
||||
Album = 'album',
|
||||
Person = 'person',
|
||||
}
|
||||
|
||||
export enum PluginTriggerType {
|
||||
export enum WorkflowTrigger {
|
||||
AssetCreate = 'AssetCreate',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
export enum WorkflowType {
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetPersonV1 = 'AssetPersonV1',
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PluginContext, PluginTriggerType } from 'src/enum';
|
||||
|
||||
export type PluginTrigger = {
|
||||
type: PluginTriggerType;
|
||||
contextType: PluginContext;
|
||||
};
|
||||
|
||||
export const pluginTriggers: PluginTrigger[] = [
|
||||
{
|
||||
type: PluginTriggerType.AssetCreate,
|
||||
contextType: PluginContext.Asset,
|
||||
},
|
||||
{
|
||||
type: PluginTriggerType.PersonRecognized,
|
||||
contextType: PluginContext.Person,
|
||||
},
|
||||
];
|
||||
@@ -1,48 +1,17 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- PluginRepository.getPlugin
|
||||
-- PluginRepository.getForLoad
|
||||
select
|
||||
"plugin"."id" as "id",
|
||||
"plugin"."name" as "name",
|
||||
"plugin"."title" as "title",
|
||||
"plugin"."description" as "description",
|
||||
"plugin"."author" as "author",
|
||||
"plugin"."version" as "version",
|
||||
"plugin"."wasmPath" as "wasmPath",
|
||||
"plugin"."createdAt" as "createdAt",
|
||||
"plugin"."updatedAt" as "updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_filter"
|
||||
where
|
||||
"plugin_filter"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "filters",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"plugin_action"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "actions"
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"wasmBytes"
|
||||
from
|
||||
"plugin"
|
||||
where
|
||||
"plugin"."id" = $1
|
||||
"enabled" = $1
|
||||
|
||||
-- PluginRepository.getPluginByName
|
||||
-- PluginRepository.search
|
||||
select
|
||||
"plugin"."id" as "id",
|
||||
"plugin"."name" as "name",
|
||||
@@ -50,7 +19,6 @@ select
|
||||
"plugin"."description" as "description",
|
||||
"plugin"."author" as "author",
|
||||
"plugin"."version" as "version",
|
||||
"plugin"."wasmPath" as "wasmPath",
|
||||
"plugin"."createdAt" as "createdAt",
|
||||
"plugin"."updatedAt" as "updatedAt",
|
||||
(
|
||||
@@ -61,99 +29,68 @@ select
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_filter"
|
||||
"plugin_method"
|
||||
where
|
||||
"plugin_filter"."pluginId" = "plugin"."id"
|
||||
"plugin_method"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "filters",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"plugin_action"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "actions"
|
||||
from
|
||||
"plugin"
|
||||
where
|
||||
"plugin"."name" = $1
|
||||
|
||||
-- PluginRepository.getAllPlugins
|
||||
select
|
||||
"plugin"."id" as "id",
|
||||
"plugin"."name" as "name",
|
||||
"plugin"."title" as "title",
|
||||
"plugin"."description" as "description",
|
||||
"plugin"."author" as "author",
|
||||
"plugin"."version" as "version",
|
||||
"plugin"."wasmPath" as "wasmPath",
|
||||
"plugin"."createdAt" as "createdAt",
|
||||
"plugin"."updatedAt" as "updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_filter"
|
||||
where
|
||||
"plugin_filter"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "filters",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"plugin_action"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "actions"
|
||||
) as "methods"
|
||||
from
|
||||
"plugin"
|
||||
order by
|
||||
"plugin"."name"
|
||||
|
||||
-- PluginRepository.getFilter
|
||||
-- PluginRepository.getByName
|
||||
select
|
||||
*
|
||||
"plugin"."id" as "id",
|
||||
"plugin"."name" as "name",
|
||||
"plugin"."title" as "title",
|
||||
"plugin"."description" as "description",
|
||||
"plugin"."author" as "author",
|
||||
"plugin"."version" as "version",
|
||||
"plugin"."createdAt" as "createdAt",
|
||||
"plugin"."updatedAt" as "updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_method"
|
||||
where
|
||||
"plugin_method"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "methods"
|
||||
from
|
||||
"plugin_filter"
|
||||
"plugin"
|
||||
where
|
||||
"id" = $1
|
||||
"plugin"."name" = $1
|
||||
|
||||
-- PluginRepository.getFiltersByPlugin
|
||||
-- PluginRepository.get
|
||||
select
|
||||
*
|
||||
"plugin"."id" as "id",
|
||||
"plugin"."name" as "name",
|
||||
"plugin"."title" as "title",
|
||||
"plugin"."description" as "description",
|
||||
"plugin"."author" as "author",
|
||||
"plugin"."version" as "version",
|
||||
"plugin"."createdAt" as "createdAt",
|
||||
"plugin"."updatedAt" as "updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_method"
|
||||
where
|
||||
"plugin_method"."pluginId" = "plugin"."id"
|
||||
) as agg
|
||||
) as "methods"
|
||||
from
|
||||
"plugin_filter"
|
||||
"plugin"
|
||||
where
|
||||
"pluginId" = $1
|
||||
|
||||
-- PluginRepository.getAction
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- PluginRepository.getActionsByPlugin
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"pluginId" = $1
|
||||
"plugin"."id" = $1
|
||||
|
||||
@@ -1,70 +1,96 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- WorkflowRepository.getWorkflow
|
||||
-- WorkflowRepository.search
|
||||
select
|
||||
*
|
||||
"workflow"."id",
|
||||
"workflow"."name",
|
||||
"workflow"."description",
|
||||
"workflow"."trigger",
|
||||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"plugin"."name" as "pluginName",
|
||||
"plugin_method"."name" as "methodName",
|
||||
"workflow_step"."config",
|
||||
"workflow_step"."enabled"
|
||||
from
|
||||
"workflow_step"
|
||||
inner join "plugin_method" on "plugin_method"."id" = "workflow_step"."pluginMethodId"
|
||||
inner join "plugin" on "plugin"."id" = "plugin_method"."pluginId"
|
||||
) as agg
|
||||
) as "steps"
|
||||
from
|
||||
"workflow"
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- WorkflowRepository.get
|
||||
select
|
||||
"workflow"."id",
|
||||
"workflow"."name",
|
||||
"workflow"."description",
|
||||
"workflow"."trigger",
|
||||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"plugin"."name" as "pluginName",
|
||||
"plugin_method"."name" as "methodName",
|
||||
"workflow_step"."config",
|
||||
"workflow_step"."enabled"
|
||||
from
|
||||
"workflow_step"
|
||||
inner join "plugin_method" on "plugin_method"."id" = "workflow_step"."pluginMethodId"
|
||||
inner join "plugin" on "plugin"."id" = "plugin_method"."pluginId"
|
||||
) as agg
|
||||
) as "steps"
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"id" = $1
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- WorkflowRepository.getWorkflowsByOwner
|
||||
-- WorkflowRepository.getForWorkflowRun
|
||||
select
|
||||
*
|
||||
"workflow"."id",
|
||||
"workflow"."name",
|
||||
"workflow"."trigger",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"workflow_step"."id",
|
||||
"workflow_step"."config",
|
||||
"plugin_method"."pluginId" as "pluginId",
|
||||
"plugin_method"."name" as "methodName",
|
||||
"plugin_method"."types" as "types"
|
||||
from
|
||||
"workflow_step"
|
||||
inner join "plugin_method" on "plugin_method"."id" = "workflow_step"."pluginMethodId"
|
||||
where
|
||||
"workflow_step"."workflowId" = "workflow"."id"
|
||||
and "workflow_step"."enabled" = $1
|
||||
) as agg
|
||||
) as "steps"
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"ownerId" = $1
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- WorkflowRepository.getWorkflowsByTrigger
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"triggerType" = $1
|
||||
and "enabled" = $2
|
||||
|
||||
-- WorkflowRepository.getWorkflowByOwnerAndTrigger
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"ownerId" = $1
|
||||
and "triggerType" = $2
|
||||
"id" = $2
|
||||
and "enabled" = $3
|
||||
|
||||
-- WorkflowRepository.deleteWorkflow
|
||||
-- WorkflowRepository.delete
|
||||
delete from "workflow"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- WorkflowRepository.getFilters
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow_filter"
|
||||
where
|
||||
"workflowId" = $1
|
||||
order by
|
||||
"order" asc
|
||||
|
||||
-- WorkflowRepository.deleteFiltersByWorkflow
|
||||
delete from "workflow_filter"
|
||||
where
|
||||
"workflowId" = $1
|
||||
|
||||
-- WorkflowRepository.getActions
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow_action"
|
||||
where
|
||||
"workflowId" = $1
|
||||
order by
|
||||
"order" asc
|
||||
|
||||
@@ -320,7 +320,7 @@ const getEnv = (): EnvData => {
|
||||
root: folders.web,
|
||||
indexHtml: join(folders.web, 'index.html'),
|
||||
},
|
||||
corePlugin: join(buildFolder, 'corePlugin'),
|
||||
corePlugin: join(buildFolder, 'plugins', 'immich-plugin-core'),
|
||||
},
|
||||
|
||||
setup: {
|
||||
|
||||
@@ -119,6 +119,10 @@ export class LoggingRepository {
|
||||
logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
|
||||
}
|
||||
|
||||
getLogLevel(): LogLevel {
|
||||
return logLevels[0] || LogLevel.Fatal;
|
||||
}
|
||||
|
||||
verbose(message: string, ...details: LogDetails) {
|
||||
this.handleMessage(LogLevel.Verbose, message, details);
|
||||
}
|
||||
|
||||
@@ -1,176 +1,236 @@
|
||||
import { CallContext, Plugin as ExtismPlugin, newPlugin } from '@extism/extism';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely } from 'kysely';
|
||||
import { Insertable, 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 { PluginMethodSearchDto, PluginSearchDto } from 'src/dtos/plugin.dto';
|
||||
import { LogLevel, WorkflowType } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { PluginMethodTable } from 'src/schema/tables/plugin-method.table';
|
||||
import { PluginTable } from 'src/schema/tables/plugin.table';
|
||||
|
||||
type PluginMethod = { pluginId: string; methodName: string };
|
||||
type PluginLoad = { id: string; name: string; version: string; wasmBytes: Buffer };
|
||||
type PluginMapItem = { plugin: ExtismPlugin; name: string; version: string };
|
||||
export type PluginHostFunction = (callContext: CallContext, input: bigint) => any; // TODO probably needs to be bigint return as well
|
||||
export type PluginLoadOptions = {
|
||||
functions: Record<string, PluginHostFunction>;
|
||||
};
|
||||
|
||||
export type PluginMethodSearchResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
pluginName: string;
|
||||
types: WorkflowType[];
|
||||
};
|
||||
|
||||
const levels = {
|
||||
[LogLevel.Verbose]: 'trace',
|
||||
[LogLevel.Debug]: 'debug',
|
||||
[LogLevel.Log]: 'info',
|
||||
[LogLevel.Warn]: 'warn',
|
||||
[LogLevel.Error]: 'error',
|
||||
[LogLevel.Fatal]: 'error',
|
||||
} as const;
|
||||
|
||||
const asExtismLogLevel = (logLevel: LogLevel) => levels[logLevel] || 'info';
|
||||
|
||||
@Injectable()
|
||||
export class PluginRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
private pluginMap: Map<string, PluginMapItem> = new Map();
|
||||
|
||||
/**
|
||||
* 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();
|
||||
constructor(
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
private logger: LoggingRepository,
|
||||
) {
|
||||
this.logger.setContext(PluginRepository.name);
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllPlugins() {
|
||||
getForLoad() {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select(['id', 'name', 'version', 'wasmBytes'])
|
||||
.where('enabled', '=', true)
|
||||
.execute();
|
||||
}
|
||||
|
||||
private queryBuilder() {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
'plugin.id',
|
||||
'plugin.name',
|
||||
'plugin.title',
|
||||
'plugin.description',
|
||||
'plugin.author',
|
||||
'plugin.version',
|
||||
'plugin.createdAt',
|
||||
'plugin.updatedAt',
|
||||
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'),
|
||||
])
|
||||
eb
|
||||
.selectFrom('plugin_method')
|
||||
.select([
|
||||
'plugin_method.name',
|
||||
'plugin_method.title',
|
||||
'plugin_method.description',
|
||||
'plugin_method.types',
|
||||
'plugin_method.schema',
|
||||
'plugin.name as pluginName',
|
||||
])
|
||||
.whereRef('plugin_method.pluginId', '=', 'plugin.id'),
|
||||
).as('methods'),
|
||||
]);
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
search(dto: PluginSearchDto = {}) {
|
||||
return this.queryBuilder()
|
||||
.$if(!!dto.id, (qb) => qb.where('plugin.id', '=', dto.id!))
|
||||
.$if(!!dto.name, (qb) => qb.where('plugin.name', '=', dto.name!))
|
||||
.$if(!!dto.title, (qb) => qb.where('plugin.title', '=', dto.title!))
|
||||
.$if(!!dto.description, (qb) => qb.where('plugin.description', '=', dto.description!))
|
||||
.$if(!!dto.version, (qb) => qb.where('plugin.version', '=', dto.version!))
|
||||
.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.STRING] })
|
||||
getByName(name: string) {
|
||||
return this.queryBuilder().where('plugin.name', '=', name).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFiltersByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_filter').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
get(id: string) {
|
||||
return this.queryBuilder().where('plugin.id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAction(id: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
@GenerateSql()
|
||||
getForValidation(): Promise<PluginMethodSearchResponse[]> {
|
||||
return this.db
|
||||
.selectFrom('plugin_method')
|
||||
.innerJoin('plugin', 'plugin_method.pluginId', 'plugin.id')
|
||||
.select(['plugin_method.id', 'plugin_method.name', 'plugin.name as pluginName', 'plugin_method.types'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getActionsByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
@GenerateSql()
|
||||
searchMethods(dto: PluginMethodSearchDto = {}) {
|
||||
return this.db
|
||||
.selectFrom('plugin_method')
|
||||
.innerJoin('plugin', 'plugin.id', 'plugin_method.pluginId')
|
||||
.select([
|
||||
'plugin_method.id',
|
||||
'plugin_method.name',
|
||||
'plugin_method.title',
|
||||
'plugin_method.description',
|
||||
'plugin_method.pluginId',
|
||||
'plugin_method.types',
|
||||
'plugin_method.schema',
|
||||
'plugin.name as pluginName',
|
||||
])
|
||||
.$if(!!dto.id, (qb) => qb.where('plugin_method.id', '=', dto.id!))
|
||||
.$if(!!dto.name, (qb) => qb.where('plugin_method.name', '=', dto.name!))
|
||||
.$if(!!dto.title, (qb) => qb.where('plugin_method.title', '=', dto.title!))
|
||||
.$if(!!dto.type, (qb) => qb.where('plugin_method.types', '@>', [dto.type!]))
|
||||
.$if(!!dto.description, (qb) => qb.where('plugin_method.description', '=', dto.description!))
|
||||
.$if(!!dto.pluginVersion, (qb) => qb.where('plugin.version', '=', dto.pluginVersion!))
|
||||
.$if(!!dto.pluginName, (qb) => qb.where('plugin.name', '=', dto.pluginName!))
|
||||
.orderBy('plugin_method.name')
|
||||
.execute();
|
||||
}
|
||||
|
||||
async create(dto: Insertable<PluginTable>, initialMethods: Omit<Insertable<PluginMethodTable>, 'pluginId'>[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
// Upsert the plugin
|
||||
const plugin = await tx
|
||||
.insertInto('plugin')
|
||||
.values(dto)
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['name', 'version']).doUpdateSet((eb) => ({
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
author: eb.ref('excluded.author'),
|
||||
version: eb.ref('excluded.version'),
|
||||
wasmBytes: eb.ref('excluded.wasmBytes'),
|
||||
})),
|
||||
)
|
||||
.returning(['id', 'name'])
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
// TODO: handle methods that were removed in a new version
|
||||
const methods =
|
||||
initialMethods.length > 0
|
||||
? await tx
|
||||
.insertInto('plugin_method')
|
||||
.values(initialMethods.map((method) => ({ ...method, pluginId: plugin.id })))
|
||||
.onConflict((oc) =>
|
||||
oc.columns(['pluginId', 'name']).doUpdateSet((eb) => ({
|
||||
pluginId: eb.ref('excluded.pluginId'),
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
types: eb.ref('excluded.types'),
|
||||
schema: eb.ref('excluded.schema'),
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
return { ...plugin, methods };
|
||||
});
|
||||
}
|
||||
|
||||
async load({ id, name, version, wasmBytes }: PluginLoad, { functions }: PluginLoadOptions) {
|
||||
const data = new Uint8Array(wasmBytes.buffer, wasmBytes.byteOffset, wasmBytes.byteLength);
|
||||
const pluginLabel = `${name}@${version}`;
|
||||
|
||||
try {
|
||||
const logger = LoggingRepository.create(`Plugin:${pluginLabel}`);
|
||||
const plugin = await newPlugin(
|
||||
{ wasm: [{ data }] },
|
||||
{
|
||||
useWasi: true,
|
||||
runInWorker: true,
|
||||
functions: {
|
||||
'extism:host/user': functions,
|
||||
},
|
||||
logLevel: asExtismLogLevel(logger.getLogLevel()),
|
||||
logger: {
|
||||
trace: (message) => logger.verbose(message),
|
||||
info: (message) => logger.log(message),
|
||||
debug: (message) => logger.debug(message),
|
||||
warn: (message) => logger.warn(message),
|
||||
error: (message) => logger.error(message),
|
||||
} as Console,
|
||||
},
|
||||
);
|
||||
this.pluginMap.set(id, { plugin, name, version });
|
||||
} catch (error: Error | any) {
|
||||
throw new Error(`Unable to instantiate plugin: ${pluginLabel}`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
async callMethod<T>({ pluginId, methodName }: PluginMethod, input: unknown) {
|
||||
const item = this.pluginMap.get(pluginId);
|
||||
if (!item) {
|
||||
throw new Error(`No loaded plugin found for ${pluginId}`);
|
||||
}
|
||||
|
||||
const { plugin, name, version } = item;
|
||||
const methodLabel = `${name}@${version}#${methodName}`;
|
||||
|
||||
try {
|
||||
const result = await plugin.call(methodName, JSON.stringify(input));
|
||||
if (result) {
|
||||
return result.json() as T;
|
||||
}
|
||||
|
||||
return result as T;
|
||||
} catch (error: Error | any) {
|
||||
throw new Error(`Plugin method call failed: ${methodLabel}`, { cause: error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { Injectable } from '@nestjs/common';
|
||||
import archiver from 'archiver';
|
||||
import chokidar, { ChokidarOptions } from 'chokidar';
|
||||
import { escapePath, glob, globStream } from 'fast-glob';
|
||||
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, ReadOptionsWithBuffer } from 'node:fs';
|
||||
import {
|
||||
constants,
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
Dirent,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
ReadOptionsWithBuffer,
|
||||
} from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { PassThrough, Readable, Writable } from 'node:stream';
|
||||
@@ -50,6 +58,10 @@ export class StorageRepository {
|
||||
return fs.readdir(folder);
|
||||
}
|
||||
|
||||
readdirWithTypes(folder: string): Promise<Dirent[]> {
|
||||
return fs.readdir(folder, { withFileTypes: true });
|
||||
}
|
||||
|
||||
copyFile(source: string, target: string) {
|
||||
return fs.copyFile(source, target);
|
||||
}
|
||||
@@ -117,17 +129,24 @@ export class StorageRepository {
|
||||
}
|
||||
|
||||
async readFile(filepath: string, options?: ReadOptionsWithBuffer<Buffer>): Promise<Buffer> {
|
||||
const file = await fs.open(filepath);
|
||||
try {
|
||||
const { buffer } = await file.read(options);
|
||||
return buffer as Buffer;
|
||||
} finally {
|
||||
await file.close();
|
||||
// read a slice
|
||||
if (options) {
|
||||
const file = await fs.open(filepath);
|
||||
try {
|
||||
const { buffer } = await file.read(options);
|
||||
return buffer as Buffer;
|
||||
} finally {
|
||||
await file.close();
|
||||
}
|
||||
}
|
||||
|
||||
// read everything
|
||||
return fs.readFile(filepath);
|
||||
}
|
||||
|
||||
async readTextFile(filepath: string): Promise<string> {
|
||||
return fs.readFile(filepath, 'utf8');
|
||||
async readJsonFile<T>(filepath: string): Promise<T> {
|
||||
const file = await fs.readFile(filepath, 'utf8');
|
||||
return JSON.parse(file) as T;
|
||||
}
|
||||
|
||||
async checkFileExists(filepath: string, mode = constants.F_OK): Promise<boolean> {
|
||||
|
||||
@@ -1,149 +1,177 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { PluginTriggerType } from 'src/enum';
|
||||
import { WorkflowSearchDto } from 'src/dtos/workflow.dto';
|
||||
import { DB } from 'src/schema';
|
||||
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';
|
||||
|
||||
export type WorkflowStepUpsert = Omit<Insertable<WorkflowStepTable>, 'workflowId' | 'order'>;
|
||||
@Injectable()
|
||||
export class WorkflowRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
private queryBuilder(db?: Kysely<DB>) {
|
||||
return (db ?? this.db)
|
||||
.selectFrom('workflow')
|
||||
.select([
|
||||
'workflow.id',
|
||||
'workflow.name',
|
||||
'workflow.description',
|
||||
'workflow.trigger',
|
||||
'workflow.enabled',
|
||||
'workflow.createdAt',
|
||||
'workflow.updatedAt',
|
||||
])
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('workflow_step')
|
||||
.innerJoin('plugin_method', 'plugin_method.id', 'workflow_step.pluginMethodId')
|
||||
.innerJoin('plugin', 'plugin.id', 'plugin_method.pluginId')
|
||||
.select([
|
||||
'plugin.name as pluginName',
|
||||
'plugin_method.name as methodName',
|
||||
'workflow_step.config',
|
||||
'workflow_step.enabled',
|
||||
]),
|
||||
).as('steps'),
|
||||
]);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getWorkflow(id: string) {
|
||||
search(dto: WorkflowSearchDto & { ownerId?: string }) {
|
||||
return this.queryBuilder()
|
||||
.$if(!!dto.ownerId, (qb) => qb.where('ownerId', '=', dto.ownerId!))
|
||||
.$if(!!dto.trigger, (qb) => qb.where('trigger', '=', dto.trigger!))
|
||||
.$if(dto.enabled !== undefined, (qb) => qb.where('enabled', '=', dto.enabled!))
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string) {
|
||||
return this.queryBuilder().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getForWorkflowRun(id: string) {
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.selectAll()
|
||||
.select(['workflow.id', 'workflow.name', 'workflow.trigger'])
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('workflow_step')
|
||||
.innerJoin('plugin_method', 'plugin_method.id', 'workflow_step.pluginMethodId')
|
||||
.whereRef('workflow_step.workflowId', '=', 'workflow.id')
|
||||
.where('workflow_step.enabled', '=', true)
|
||||
.select([
|
||||
'workflow_step.id',
|
||||
'workflow_step.config',
|
||||
'plugin_method.pluginId as pluginId',
|
||||
'plugin_method.name as methodName',
|
||||
'plugin_method.types as types',
|
||||
]),
|
||||
).as('steps'),
|
||||
])
|
||||
.where('id', '=', id)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.where('enabled', '=', true)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getWorkflowsByOwner(ownerId: string) {
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.selectAll()
|
||||
.where('ownerId', '=', ownerId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [PluginTriggerType.AssetCreate] })
|
||||
getWorkflowsByTrigger(type: PluginTriggerType) {
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.selectAll()
|
||||
.where('triggerType', '=', type)
|
||||
.where('enabled', '=', true)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, PluginTriggerType.AssetCreate] })
|
||||
getWorkflowByOwnerAndTrigger(ownerId: string, type: PluginTriggerType) {
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.selectAll()
|
||||
.where('ownerId', '=', ownerId)
|
||||
.where('triggerType', '=', type)
|
||||
.where('enabled', '=', true)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async createWorkflow(
|
||||
workflow: Insertable<WorkflowTable>,
|
||||
filters: Insertable<WorkflowFilterTable>[],
|
||||
actions: Insertable<WorkflowActionTable>[],
|
||||
) {
|
||||
return await this.db.transaction().execute(async (tx) => {
|
||||
const createdWorkflow = await tx.insertInto('workflow').values(workflow).returningAll().executeTakeFirstOrThrow();
|
||||
|
||||
if (filters.length > 0) {
|
||||
const newFilters = filters.map((filter) => ({
|
||||
...filter,
|
||||
workflowId: createdWorkflow.id,
|
||||
}));
|
||||
|
||||
await tx.insertInto('workflow_filter').values(newFilters).execute();
|
||||
}
|
||||
|
||||
if (actions.length > 0) {
|
||||
const newActions = actions.map((action) => ({
|
||||
...action,
|
||||
workflowId: createdWorkflow.id,
|
||||
}));
|
||||
await tx.insertInto('workflow_action').values(newActions).execute();
|
||||
}
|
||||
|
||||
return createdWorkflow;
|
||||
create(dto: Insertable<WorkflowTable>, steps?: WorkflowStepUpsert[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const { id } = await tx.insertInto('workflow').values(dto).returning(['id']).executeTakeFirstOrThrow();
|
||||
return this.replaceAndReturn(tx, id, steps);
|
||||
});
|
||||
}
|
||||
|
||||
async updateWorkflow(
|
||||
id: string,
|
||||
workflow: Updateable<WorkflowTable>,
|
||||
filters: Insertable<WorkflowFilterTable>[] | undefined,
|
||||
actions: Insertable<WorkflowActionTable>[] | undefined,
|
||||
) {
|
||||
return await this.db.transaction().execute(async (trx) => {
|
||||
if (Object.keys(workflow).length > 0) {
|
||||
await trx.updateTable('workflow').set(workflow).where('id', '=', id).execute();
|
||||
update(id: string, dto: Updateable<WorkflowTable>, steps?: WorkflowStepUpsert[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
if (Object.values(dto).some((prop) => prop !== undefined)) {
|
||||
await tx.updateTable('workflow').set(dto).where('id', '=', id).executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
if (filters !== undefined) {
|
||||
await trx.deleteFrom('workflow_filter').where('workflowId', '=', id).execute();
|
||||
if (filters.length > 0) {
|
||||
const filtersWithWorkflowId = filters.map((filter) => ({
|
||||
...filter,
|
||||
workflowId: id,
|
||||
}));
|
||||
await trx.insertInto('workflow_filter').values(filtersWithWorkflowId).execute();
|
||||
}
|
||||
}
|
||||
|
||||
if (actions !== undefined) {
|
||||
await trx.deleteFrom('workflow_action').where('workflowId', '=', id).execute();
|
||||
if (actions.length > 0) {
|
||||
const actionsWithWorkflowId = actions.map((action) => ({
|
||||
...action,
|
||||
workflowId: id,
|
||||
}));
|
||||
await trx.insertInto('workflow_action').values(actionsWithWorkflowId).execute();
|
||||
}
|
||||
}
|
||||
|
||||
return await trx.selectFrom('workflow').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
||||
return this.replaceAndReturn(tx, id, steps);
|
||||
});
|
||||
}
|
||||
|
||||
private async replaceAndReturn(tx: Kysely<DB>, workflowId: string, steps?: WorkflowStepUpsert[]) {
|
||||
if (steps) {
|
||||
await tx.deleteFrom('workflow_step').where('workflowId', '=', workflowId).execute();
|
||||
if (steps.length > 0) {
|
||||
await tx
|
||||
.insertInto('workflow_step')
|
||||
.values(
|
||||
steps.map((step, i) => ({
|
||||
workflowId,
|
||||
enabled: step.enabled ?? true,
|
||||
pluginMethodId: step.pluginMethodId,
|
||||
config: step.config,
|
||||
order: i,
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
return this.queryBuilder(tx).where('id', '=', workflowId).executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async deleteWorkflow(id: string) {
|
||||
async delete(id: string) {
|
||||
await this.db.deleteFrom('workflow').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFilters(workflowId: string) {
|
||||
getForAssetV1(assetId: string) {
|
||||
return this.db
|
||||
.selectFrom('workflow_filter')
|
||||
.selectAll()
|
||||
.where('workflowId', '=', workflowId)
|
||||
.orderBy('order', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async deleteFiltersByWorkflow(workflowId: string) {
|
||||
await this.db.deleteFrom('workflow_filter').where('workflowId', '=', workflowId).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getActions(workflowId: string) {
|
||||
return this.db
|
||||
.selectFrom('workflow_action')
|
||||
.selectAll()
|
||||
.where('workflowId', '=', workflowId)
|
||||
.orderBy('order', 'asc')
|
||||
.execute();
|
||||
.selectFrom('asset')
|
||||
.leftJoin('asset_exif', 'asset_exif.assetId', 'asset.id')
|
||||
.select((eb) => [
|
||||
...columns.workflowAssetV1,
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('asset_exif')
|
||||
.select([
|
||||
'asset_exif.make',
|
||||
'asset_exif.model',
|
||||
'asset_exif.orientation',
|
||||
'asset_exif.dateTimeOriginal',
|
||||
'asset_exif.modifyDate',
|
||||
'asset_exif.exifImageWidth',
|
||||
'asset_exif.exifImageHeight',
|
||||
'asset_exif.fileSizeInByte',
|
||||
'asset_exif.lensModel',
|
||||
'asset_exif.fNumber',
|
||||
'asset_exif.focalLength',
|
||||
'asset_exif.iso',
|
||||
'asset_exif.latitude',
|
||||
'asset_exif.longitude',
|
||||
'asset_exif.city',
|
||||
'asset_exif.state',
|
||||
'asset_exif.country',
|
||||
'asset_exif.description',
|
||||
'asset_exif.fps',
|
||||
'asset_exif.exposureTime',
|
||||
'asset_exif.livePhotoCID',
|
||||
'asset_exif.timeZone',
|
||||
'asset_exif.projectionType',
|
||||
'asset_exif.profileDescription',
|
||||
'asset_exif.colorspace',
|
||||
'asset_exif.bitsPerSample',
|
||||
'asset_exif.autoStackId',
|
||||
'asset_exif.rating',
|
||||
'asset_exif.tags',
|
||||
'asset_exif.updatedAt',
|
||||
])
|
||||
.whereRef('asset_exif.assetId', '=', 'asset.id'),
|
||||
).as('exifInfo'),
|
||||
])
|
||||
.where('id', '=', assetId)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,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';
|
||||
@@ -73,7 +74,8 @@ import { UserMetadataAuditTable } from 'src/schema/tables/user-metadata-audit.ta
|
||||
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { VersionHistoryTable } from 'src/schema/tables/version-history.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' })
|
||||
@@ -132,11 +134,9 @@ export class ImmichDatabase {
|
||||
UserTable,
|
||||
VersionHistoryTable,
|
||||
PluginTable,
|
||||
PluginFilterTable,
|
||||
PluginActionTable,
|
||||
PluginMethodTable,
|
||||
WorkflowTable,
|
||||
WorkflowFilterTable,
|
||||
WorkflowActionTable,
|
||||
WorkflowStepTable,
|
||||
];
|
||||
|
||||
functions = [
|
||||
@@ -249,10 +249,8 @@ export interface DB {
|
||||
version_history: VersionHistoryTable;
|
||||
|
||||
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,15 @@
|
||||
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);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// unsupported
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
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,
|
||||
"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(),
|
||||
"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
|
||||
}
|
||||
9
server/src/schema/migrations/1773175313374-Test.ts
Normal file
9
server/src/schema/migrations/1773175313374-Test.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
29
server/src/schema/tables/plugin-method.table.ts
Normal file
29
server/src/schema/tables/plugin-method.table.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table, Unique } from '@immich/sql-tools';
|
||||
import { WorkflowType } from 'src/enum';
|
||||
import { PluginTable } from 'src/schema/tables/plugin.table';
|
||||
import { JSONSchema } from 'src/types';
|
||||
|
||||
@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: 'jsonb', nullable: true })
|
||||
schema!: JSONSchema | null;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
26
server/src/schema/tables/workflow-step.table.ts
Normal file
26
server/src/schema/tables/workflow-step.table.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { Insertable } from 'kysely';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SystemConfig } from 'src/config';
|
||||
@@ -187,6 +188,67 @@ export class BaseService {
|
||||
);
|
||||
}
|
||||
|
||||
static create<T extends BaseService>(Service: ClassConstructor<T>, ctx: BaseService) {
|
||||
const service = new Service(
|
||||
LoggingRepository.create(),
|
||||
ctx.accessRepository,
|
||||
ctx.activityRepository,
|
||||
ctx.albumRepository,
|
||||
ctx.albumUserRepository,
|
||||
ctx.apiKeyRepository,
|
||||
ctx.appRepository,
|
||||
ctx.assetRepository,
|
||||
ctx.assetEditRepository,
|
||||
ctx.assetJobRepository,
|
||||
ctx.auditRepository,
|
||||
ctx.configRepository,
|
||||
ctx.cronRepository,
|
||||
ctx.cryptoRepository,
|
||||
ctx.databaseRepository,
|
||||
ctx.downloadRepository,
|
||||
ctx.duplicateRepository,
|
||||
ctx.emailRepository,
|
||||
ctx.eventRepository,
|
||||
ctx.jobRepository,
|
||||
ctx.libraryRepository,
|
||||
ctx.machineLearningRepository,
|
||||
ctx.mapRepository,
|
||||
ctx.mediaRepository,
|
||||
ctx.memoryRepository,
|
||||
ctx.metadataRepository,
|
||||
ctx.moveRepository,
|
||||
ctx.notificationRepository,
|
||||
ctx.oauthRepository,
|
||||
ctx.ocrRepository,
|
||||
ctx.partnerRepository,
|
||||
ctx.personRepository,
|
||||
ctx.pluginRepository,
|
||||
ctx.processRepository,
|
||||
ctx.searchRepository,
|
||||
ctx.serverInfoRepository,
|
||||
ctx.sessionRepository,
|
||||
ctx.sharedLinkRepository,
|
||||
ctx.sharedLinkAssetRepository,
|
||||
ctx.stackRepository,
|
||||
ctx.storageRepository,
|
||||
ctx.syncRepository,
|
||||
ctx.syncCheckpointRepository,
|
||||
ctx.systemMetadataRepository,
|
||||
ctx.tagRepository,
|
||||
ctx.telemetryRepository,
|
||||
ctx.trashRepository,
|
||||
ctx.userRepository,
|
||||
ctx.versionRepository,
|
||||
ctx.viewRepository,
|
||||
ctx.websocketRepository,
|
||||
ctx.workflowRepository,
|
||||
);
|
||||
|
||||
service.logger.setContext(this.name);
|
||||
|
||||
return service as T;
|
||||
}
|
||||
|
||||
get worker() {
|
||||
return this.configRepository.getWorker();
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { UserAdminService } from 'src/services/user-admin.service';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { ViewService } from 'src/services/view.service';
|
||||
import { WorkflowExecutionService } from 'src/services/workflow-execution.service';
|
||||
import { WorkflowService } from 'src/services/workflow.service';
|
||||
|
||||
export const services = [
|
||||
@@ -95,5 +96,6 @@ export const services = [
|
||||
UserService,
|
||||
VersionService,
|
||||
ViewService,
|
||||
WorkflowExecutionService,
|
||||
WorkflowService,
|
||||
];
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { CurrentPlugin } from '@extism/extism';
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { Updateable } from 'kysely';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { requireAccess } from 'src/utils/access';
|
||||
|
||||
/**
|
||||
* Plugin host functions that are exposed to WASM plugins via Extism.
|
||||
* These functions allow plugins to interact with the Immich system.
|
||||
*/
|
||||
export class PluginHostFunctions {
|
||||
constructor(
|
||||
private assetRepository: AssetRepository,
|
||||
private albumRepository: AlbumRepository,
|
||||
private accessRepository: AccessRepository,
|
||||
private cryptoRepository: CryptoRepository,
|
||||
private logger: LoggingRepository,
|
||||
private pluginJwtSecret: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates Extism host function bindings for the plugin.
|
||||
* These are the functions that WASM plugins can call.
|
||||
*/
|
||||
getHostFunctions() {
|
||||
return {
|
||||
'extism:host/user': {
|
||||
updateAsset: (cp: CurrentPlugin, offs: bigint) => this.handleUpdateAsset(cp, offs),
|
||||
addAssetToAlbum: (cp: CurrentPlugin, offs: bigint) => this.handleAddAssetToAlbum(cp, offs),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Host function wrapper for updateAsset.
|
||||
* Reads the input from the plugin, parses it, and calls the actual update function.
|
||||
*/
|
||||
private async handleUpdateAsset(cp: CurrentPlugin, offs: bigint) {
|
||||
const input = JSON.parse(cp.read(offs)!.text());
|
||||
await this.updateAsset(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Host function wrapper for addAssetToAlbum.
|
||||
* Reads the input from the plugin, parses it, and calls the actual add function.
|
||||
*/
|
||||
private async handleAddAssetToAlbum(cp: CurrentPlugin, offs: bigint) {
|
||||
const input = JSON.parse(cp.read(offs)!.text());
|
||||
await this.addAssetToAlbum(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the JWT token and returns the auth context.
|
||||
*/
|
||||
private validateToken(authToken: string): { userId: string } {
|
||||
try {
|
||||
const auth = this.cryptoRepository.verifyJwt<{ userId: string }>(authToken, this.pluginJwtSecret);
|
||||
if (!auth.userId) {
|
||||
throw new UnauthorizedException('Invalid token: missing userId');
|
||||
}
|
||||
return auth;
|
||||
} catch (error) {
|
||||
this.logger.error('Token validation failed:', error);
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an asset with the given properties.
|
||||
*/
|
||||
async updateAsset(input: { authToken: string } & Updateable<AssetTable> & { id: string }) {
|
||||
const { authToken, id, ...assetData } = input;
|
||||
|
||||
// Validate token
|
||||
const auth = this.validateToken(authToken);
|
||||
|
||||
// Check access to the asset
|
||||
await requireAccess(this.accessRepository, {
|
||||
auth: { user: { id: auth.userId } } as any,
|
||||
permission: Permission.AssetUpdate,
|
||||
ids: [id],
|
||||
});
|
||||
|
||||
this.logger.log(`Updating asset ${id} -- ${JSON.stringify(assetData)}`);
|
||||
await this.assetRepository.update({ id, ...assetData });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an asset to an album.
|
||||
*/
|
||||
async addAssetToAlbum(input: { authToken: string; assetId: string; albumId: string }) {
|
||||
const { authToken, assetId, albumId } = input;
|
||||
|
||||
// Validate token
|
||||
const auth = this.validateToken(authToken);
|
||||
|
||||
// Check access to both the asset and the album
|
||||
await requireAccess(this.accessRepository, {
|
||||
auth: { user: { id: auth.userId } } as any,
|
||||
permission: Permission.AssetRead,
|
||||
ids: [assetId],
|
||||
});
|
||||
|
||||
await requireAccess(this.accessRepository, {
|
||||
auth: { user: { id: auth.userId } } as any,
|
||||
permission: Permission.AlbumUpdate,
|
||||
ids: [albumId],
|
||||
});
|
||||
|
||||
this.logger.log(`Adding asset ${assetId} to album ${albumId}`);
|
||||
await this.albumRepository.addAssetIds(albumId, [assetId]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,322 +1,34 @@
|
||||
import { Plugin as ExtismPlugin, newPlugin } from '@extism/extism';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateOrReject } from 'class-validator';
|
||||
import { join } from 'node:path';
|
||||
import { Asset, WorkflowAction, WorkflowFilter } from 'src/database';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto';
|
||||
import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum';
|
||||
import { pluginTriggers } from 'src/plugins';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import {
|
||||
mapMethod,
|
||||
mapPlugin,
|
||||
PluginMethodResponseDto,
|
||||
PluginMethodSearchDto,
|
||||
PluginResponseDto,
|
||||
PluginSearchDto,
|
||||
} from 'src/dtos/plugin.dto';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { PluginHostFunctions } from 'src/services/plugin-host.functions';
|
||||
import { IWorkflowJob, JobItem, JobOf, WorkflowData } from 'src/types';
|
||||
|
||||
interface WorkflowContext {
|
||||
authToken: string;
|
||||
asset: Asset;
|
||||
}
|
||||
|
||||
interface PluginInput<T = unknown> {
|
||||
authToken: string;
|
||||
config: T;
|
||||
data: {
|
||||
asset: Asset;
|
||||
};
|
||||
}
|
||||
import { isMethodCompatible } from 'src/utils/workflow';
|
||||
|
||||
@Injectable()
|
||||
export class PluginService extends BaseService {
|
||||
private pluginJwtSecret!: string;
|
||||
private loadedPlugins: Map<string, ExtismPlugin> = new Map();
|
||||
private hostFunctions!: PluginHostFunctions;
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap' })
|
||||
async onBootstrap() {
|
||||
this.pluginJwtSecret = this.cryptoRepository.randomBytesAsText(32);
|
||||
|
||||
await this.loadPluginsFromManifests();
|
||||
|
||||
this.hostFunctions = new PluginHostFunctions(
|
||||
this.assetRepository,
|
||||
this.albumRepository,
|
||||
this.accessRepository,
|
||||
this.cryptoRepository,
|
||||
this.logger,
|
||||
this.pluginJwtSecret,
|
||||
);
|
||||
|
||||
await this.loadPlugins();
|
||||
}
|
||||
|
||||
getTriggers(): PluginTriggerResponseDto[] {
|
||||
return pluginTriggers;
|
||||
}
|
||||
|
||||
//
|
||||
// CRUD operations for plugins
|
||||
//
|
||||
async getAll(): Promise<PluginResponseDto[]> {
|
||||
const plugins = await this.pluginRepository.getAllPlugins();
|
||||
async search(dto: PluginSearchDto): Promise<PluginResponseDto[]> {
|
||||
const plugins = await this.pluginRepository.search(dto);
|
||||
return plugins.map((plugin) => mapPlugin(plugin));
|
||||
}
|
||||
|
||||
async get(id: string): Promise<PluginResponseDto> {
|
||||
const plugin = await this.pluginRepository.getPlugin(id);
|
||||
const plugin = await this.pluginRepository.get(id);
|
||||
if (!plugin) {
|
||||
throw new BadRequestException('Plugin not found');
|
||||
}
|
||||
return mapPlugin(plugin);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// Plugin Loader
|
||||
//////////////////////////////////////////
|
||||
async loadPluginsFromManifests(): Promise<void> {
|
||||
// Load core plugin
|
||||
const { resourcePaths, plugins } = this.configRepository.getEnv();
|
||||
const coreManifestPath = `${resourcePaths.corePlugin}/manifest.json`;
|
||||
|
||||
const coreManifest = await this.readAndValidateManifest(coreManifestPath);
|
||||
await this.loadPluginToDatabase(coreManifest, resourcePaths.corePlugin);
|
||||
|
||||
this.logger.log(`Successfully processed core plugin: ${coreManifest.name} (version ${coreManifest.version})`);
|
||||
|
||||
// Load external plugins
|
||||
if (plugins.external.allow && plugins.external.installFolder) {
|
||||
await this.loadExternalPlugins(plugins.external.installFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadExternalPlugins(installFolder: string): Promise<void> {
|
||||
try {
|
||||
const entries = await this.pluginRepository.readDirectory(installFolder);
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginFolder = join(installFolder, entry.name);
|
||||
const manifestPath = join(pluginFolder, 'manifest.json');
|
||||
try {
|
||||
const manifest = await this.readAndValidateManifest(manifestPath);
|
||||
await this.loadPluginToDatabase(manifest, pluginFolder);
|
||||
|
||||
this.logger.log(`Successfully processed external plugin: ${manifest.name} (version ${manifest.version})`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to load external plugin from ${manifestPath}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to scan external plugins folder ${installFolder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPluginToDatabase(manifest: PluginManifestDto, basePath: string): Promise<void> {
|
||||
const currentPlugin = await this.pluginRepository.getPluginByName(manifest.name);
|
||||
if (currentPlugin != null && currentPlugin.version === manifest.version) {
|
||||
this.logger.log(`Plugin ${manifest.name} is up to date (version ${manifest.version}). Skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { plugin, filters, actions } = await this.pluginRepository.loadPlugin(manifest, basePath);
|
||||
|
||||
this.logger.log(`Upserted plugin: ${plugin.name} (ID: ${plugin.id}, version: ${plugin.version})`);
|
||||
|
||||
for (const filter of filters) {
|
||||
this.logger.log(`Upserted plugin filter: ${filter.methodName} (ID: ${filter.id})`);
|
||||
}
|
||||
|
||||
for (const action of actions) {
|
||||
this.logger.log(`Upserted plugin action: ${action.methodName} (ID: ${action.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
private async readAndValidateManifest(manifestPath: string): Promise<PluginManifestDto> {
|
||||
const content = await this.storageRepository.readTextFile(manifestPath);
|
||||
const manifestData = JSON.parse(content);
|
||||
const manifest = plainToInstance(PluginManifestDto, manifestData);
|
||||
|
||||
await validateOrReject(manifest, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
});
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// Plugin Execution
|
||||
///////////////////////////////////////////
|
||||
private async loadPlugins() {
|
||||
const plugins = await this.pluginRepository.getAllPlugins();
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
this.logger.debug(`Loading plugin: ${plugin.name} from ${plugin.wasmPath}`);
|
||||
|
||||
const extismPlugin = await newPlugin(plugin.wasmPath, {
|
||||
useWasi: true,
|
||||
functions: this.hostFunctions.getHostFunctions(),
|
||||
});
|
||||
|
||||
this.loadedPlugins.set(plugin.id, extismPlugin);
|
||||
this.logger.log(`Successfully loaded plugin: ${plugin.name}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to load plugin ${plugin.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AssetCreate' })
|
||||
async handleAssetCreate({ asset }: ArgOf<'AssetCreate'>) {
|
||||
await this.handleTrigger(PluginTriggerType.AssetCreate, {
|
||||
ownerId: asset.ownerId,
|
||||
event: { userId: asset.ownerId, asset },
|
||||
});
|
||||
}
|
||||
|
||||
private async handleTrigger<T extends PluginTriggerType>(
|
||||
triggerType: T,
|
||||
params: { ownerId: string; event: WorkflowData[T] },
|
||||
): Promise<void> {
|
||||
const workflows = await this.workflowRepository.getWorkflowByOwnerAndTrigger(params.ownerId, triggerType);
|
||||
if (workflows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobs: JobItem[] = workflows.map((workflow) => ({
|
||||
name: JobName.WorkflowRun,
|
||||
data: {
|
||||
id: workflow.id,
|
||||
type: triggerType,
|
||||
event: params.event,
|
||||
} as IWorkflowJob<T>,
|
||||
}));
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
this.logger.debug(`Queued ${jobs.length} workflow execution jobs for trigger ${triggerType}`);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.WorkflowRun, queue: QueueName.Workflow })
|
||||
async handleWorkflowRun({ id: workflowId, type, event }: JobOf<JobName.WorkflowRun>): Promise<JobStatus> {
|
||||
try {
|
||||
const workflow = await this.workflowRepository.getWorkflow(workflowId);
|
||||
if (!workflow) {
|
||||
this.logger.error(`Workflow ${workflowId} not found`);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
const workflowFilters = await this.workflowRepository.getFilters(workflowId);
|
||||
const workflowActions = await this.workflowRepository.getActions(workflowId);
|
||||
|
||||
switch (type) {
|
||||
case PluginTriggerType.AssetCreate: {
|
||||
const data = event as WorkflowData[PluginTriggerType.AssetCreate];
|
||||
const asset = data.asset;
|
||||
|
||||
const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret);
|
||||
|
||||
const context = {
|
||||
authToken,
|
||||
asset,
|
||||
};
|
||||
|
||||
const filtersPassed = await this.executeFilters(workflowFilters, context);
|
||||
if (!filtersPassed) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
await this.executeActions(workflowActions, context);
|
||||
this.logger.debug(`Workflow ${workflowId} executed successfully`);
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
case PluginTriggerType.PersonRecognized: {
|
||||
this.logger.error('unimplemented');
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
default: {
|
||||
this.logger.error(`Unknown workflow trigger type: ${type}`);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error executing workflow ${workflowId}:`, error);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
private async executeFilters(workflowFilters: WorkflowFilter[], context: WorkflowContext): Promise<boolean> {
|
||||
for (const workflowFilter of workflowFilters) {
|
||||
const filter = await this.pluginRepository.getFilter(workflowFilter.pluginFilterId);
|
||||
if (!filter) {
|
||||
this.logger.error(`Filter ${workflowFilter.pluginFilterId} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const pluginInstance = this.loadedPlugins.get(filter.pluginId);
|
||||
if (!pluginInstance) {
|
||||
this.logger.error(`Plugin ${filter.pluginId} not loaded`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterInput: PluginInput = {
|
||||
authToken: context.authToken,
|
||||
config: workflowFilter.filterConfig,
|
||||
data: {
|
||||
asset: context.asset,
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.debug(`Calling filter ${filter.methodName} with input: ${JSON.stringify(filterInput)}`);
|
||||
|
||||
const filterResult = await pluginInstance.call(
|
||||
filter.methodName,
|
||||
new TextEncoder().encode(JSON.stringify(filterInput)),
|
||||
);
|
||||
|
||||
if (!filterResult) {
|
||||
this.logger.error(`Filter ${filter.methodName} returned null`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = JSON.parse(filterResult.text());
|
||||
if (result.passed === false) {
|
||||
this.logger.debug(`Filter ${filter.methodName} returned false, stopping workflow execution`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async executeActions(workflowActions: WorkflowAction[], context: WorkflowContext): Promise<void> {
|
||||
for (const workflowAction of workflowActions) {
|
||||
const action = await this.pluginRepository.getAction(workflowAction.pluginActionId);
|
||||
if (!action) {
|
||||
throw new Error(`Action ${workflowAction.pluginActionId} not found`);
|
||||
}
|
||||
|
||||
const pluginInstance = this.loadedPlugins.get(action.pluginId);
|
||||
if (!pluginInstance) {
|
||||
throw new Error(`Plugin ${action.pluginId} not loaded`);
|
||||
}
|
||||
|
||||
const actionInput: PluginInput = {
|
||||
authToken: context.authToken,
|
||||
config: workflowAction.actionConfig,
|
||||
data: {
|
||||
asset: context.asset,
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.debug(`Calling action ${action.methodName} with input: ${JSON.stringify(actionInput)}`);
|
||||
|
||||
await pluginInstance.call(action.methodName, JSON.stringify(actionInput));
|
||||
}
|
||||
async searchMethods(dto: PluginMethodSearchDto): Promise<PluginMethodResponseDto[]> {
|
||||
const methods = await this.pluginRepository.searchMethods(dto);
|
||||
return methods
|
||||
.filter((method) => !dto.trigger || isMethodCompatible(method, dto.trigger))
|
||||
.map((method) => mapMethod(method));
|
||||
}
|
||||
}
|
||||
|
||||
301
server/src/services/workflow-execution.service.ts
Normal file
301
server/src/services/workflow-execution.service.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { CurrentPlugin } from '@extism/extism';
|
||||
import { WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk';
|
||||
import { HttpException, UnauthorizedException } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { join } from 'node:path';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import {
|
||||
BootstrapEventPriority,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
JobName,
|
||||
JobStatus,
|
||||
QueueName,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
} from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobOf } from 'src/types';
|
||||
|
||||
type ExecuteOptions<T extends WorkflowType = any> = {
|
||||
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T> }>;
|
||||
write: (changes: Partial<WorkflowEventData<T>>) => Promise<void>;
|
||||
};
|
||||
|
||||
export class WorkflowExecutionService extends BaseService {
|
||||
private jwtSecret!: string;
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.PluginSync, workers: [ImmichWorker.Microservices] })
|
||||
async onPluginSync() {
|
||||
await this.databaseRepository.withLock(DatabaseLock.PluginImport, async () => {
|
||||
// TODO avoid importing plugins in each worker
|
||||
// Can this use system metadata similar to geocoding?
|
||||
|
||||
const { resourcePaths, plugins } = this.configRepository.getEnv();
|
||||
await this.importFolder(resourcePaths.corePlugin, { force: true });
|
||||
|
||||
if (plugins.external.allow && plugins.external.installFolder) {
|
||||
await this.importFolders(plugins.external.installFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.PluginLoad, workers: [ImmichWorker.Microservices] })
|
||||
async onPluginLoad() {
|
||||
this.jwtSecret = this.cryptoRepository.randomBytesAsText(32);
|
||||
|
||||
const albumService = BaseService.create(AlbumService, this);
|
||||
|
||||
const albumAddAssets = this.createFunction<[id: string, dto: BulkIdsDto]>(async (authDto, args) =>
|
||||
albumService.addAssets(authDto, ...args),
|
||||
);
|
||||
|
||||
const plugins = await this.pluginRepository.getForLoad();
|
||||
for (const plugin of plugins) {
|
||||
const pluginLabel = `${plugin.name}@${plugin.version}`;
|
||||
|
||||
try {
|
||||
await this.pluginRepository.load(plugin, {
|
||||
functions: {
|
||||
albumAddAssets,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Loaded plugin: ${pluginLabel}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Unable to load plugin ${pluginLabel}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createFunction<T>(fn: (authDto: AuthDto, args: T) => Promise<unknown>) {
|
||||
return async (plugin: CurrentPlugin, offset: bigint) => {
|
||||
try {
|
||||
const handle = plugin.read(offset);
|
||||
if (!handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { authToken, args } = handle.json() as { authToken: string; args: T };
|
||||
if (!authToken) {
|
||||
throw new Error('authToken is required');
|
||||
}
|
||||
|
||||
const authDto = this.validate(authToken);
|
||||
const response = await fn(authDto, args);
|
||||
|
||||
return plugin.store(JSON.stringify({ success: true, response }));
|
||||
} catch (error: Error | any) {
|
||||
if (error instanceof HttpException) {
|
||||
this.logger.error(`Plugin host exception: ${error}`);
|
||||
return plugin.store(
|
||||
JSON.stringify({ success: false, status: error.getStatus(), message: error.getResponse() }),
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.error(`Plugin host exception: ${error}`, error?.stack);
|
||||
|
||||
return plugin.store(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
status: 500,
|
||||
message: `Internal server error: ${error}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async importFolders(installFolder: string): Promise<void> {
|
||||
try {
|
||||
const entries = await this.storageRepository.readdirWithTypes(installFolder);
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.importFolder(join(installFolder, entry.name));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to import plugins folder ${installFolder}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async importFolder(folder: string, options?: { force?: boolean }) {
|
||||
try {
|
||||
const manifestPath = join(folder, 'manifest.json');
|
||||
const dto = await this.storageRepository.readJsonFile(manifestPath);
|
||||
const manifest = plainToInstance(PluginManifestDto, dto);
|
||||
const errors = await validate(manifest, { whitelist: true, forbidNonWhitelisted: true });
|
||||
if (errors.length > 0) {
|
||||
this.logger.warn(`Invalid plugin manifest at ${manifestPath}:\n${errors.map((e) => e.toString()).join('\n')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await this.pluginRepository.getByName(manifest.name);
|
||||
if (existing && existing.version === manifest.version && options?.force !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasmPath = `${folder}/${manifest.wasmPath}`;
|
||||
const wasmBytes = await this.storageRepository.readFile(wasmPath);
|
||||
|
||||
const plugin = await this.pluginRepository.create(
|
||||
{
|
||||
enabled: true,
|
||||
name: manifest.name,
|
||||
title: manifest.title,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
wasmBytes,
|
||||
},
|
||||
manifest.methods.map((method) => ({
|
||||
name: method.name,
|
||||
title: method.title,
|
||||
description: method.description,
|
||||
types: method.types,
|
||||
schema: method.schema,
|
||||
})),
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
this.logger.log(
|
||||
`Upgraded plugin ${manifest.name} (${plugin.methods.length} methods) from ${existing.version} to ${manifest.version} `,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Imported plugin ${manifest.name}@${manifest.version} (${plugin.methods.length} methods) from ${folder}`,
|
||||
);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
} catch {
|
||||
this.logger.warn(`Failed to import plugin from ${folder}:`);
|
||||
}
|
||||
}
|
||||
|
||||
private validate(authToken: string): AuthDto {
|
||||
try {
|
||||
const jwt = this.cryptoRepository.verifyJwt<{ userId: string }>(authToken, this.jwtSecret);
|
||||
if (!jwt.userId) {
|
||||
throw new UnauthorizedException('Invalid token: missing userId');
|
||||
}
|
||||
|
||||
return {
|
||||
user: {
|
||||
id: jwt.userId,
|
||||
},
|
||||
} as AuthDto;
|
||||
} catch (error) {
|
||||
this.logger.error('Token validation failed:', error);
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
private sign(userId: string) {
|
||||
return this.cryptoRepository.signJwt({ userId }, this.jwtSecret);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AssetCreate' })
|
||||
async onAssetCreate({ asset }: ArgOf<'AssetCreate'>) {
|
||||
const dto = { ownerId: asset.ownerId, trigger: WorkflowTrigger.AssetCreate };
|
||||
const items = await this.workflowRepository.search(dto);
|
||||
await this.jobRepository.queueAll(
|
||||
items.map((workflow) => ({
|
||||
name: JobName.WorkflowAssetCreate,
|
||||
data: { workflowId: workflow.id, assetId: asset.id },
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.WorkflowAssetCreate, queue: QueueName.Workflow })
|
||||
async handleWorkflowAssetCreate({ workflowId, assetId }: JobOf<JobName.WorkflowAssetCreate>) {
|
||||
await this.execute(workflowId, (type: WorkflowType) => {
|
||||
switch (type) {
|
||||
case WorkflowType.AssetV1: {
|
||||
return <ExecuteOptions<WorkflowType.AssetV1>>{
|
||||
read: async () => {
|
||||
const asset = await this.workflowRepository.getForAssetV1(assetId);
|
||||
return {
|
||||
data: { asset },
|
||||
authUserId: asset.ownerId,
|
||||
};
|
||||
},
|
||||
write: async (changes) => {
|
||||
if (changes.asset) {
|
||||
await this.assetRepository.update({
|
||||
id: assetId,
|
||||
..._.omitBy(
|
||||
{
|
||||
isFavorite: changes.asset?.isFavorite,
|
||||
visibility: changes.asset?.visibility,
|
||||
},
|
||||
_.isUndefined,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async execute(workflowId: string, getHandler: (type: WorkflowType) => ExecuteOptions | undefined) {
|
||||
const workflow = await this.workflowRepository.getForWorkflowRun(workflowId);
|
||||
if (!workflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO infer from steps
|
||||
const type = 'AssetV1' as WorkflowType;
|
||||
const handler = getHandler(type);
|
||||
if (!handler) {
|
||||
this.logger.error(`Misconfigured workflow ${workflowId}: no handler for type ${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { read, write } = handler;
|
||||
const readResult = await read(type);
|
||||
let data = readResult.data;
|
||||
for (const step of workflow.steps) {
|
||||
const payload: WorkflowEventPayload = {
|
||||
trigger: workflow.trigger,
|
||||
type,
|
||||
config: step.config ?? {},
|
||||
workflow: {
|
||||
id: workflowId,
|
||||
authToken: this.sign(readResult.authUserId),
|
||||
stepId: step.id,
|
||||
},
|
||||
data,
|
||||
};
|
||||
|
||||
const result = await this.pluginRepository.callMethod<WorkflowResponse>(step, payload);
|
||||
if (result?.changes) {
|
||||
await write(result.changes);
|
||||
data = await read(type);
|
||||
}
|
||||
|
||||
const shouldContinue = result?.workflow?.continue ?? true;
|
||||
if (!shouldContinue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`Workflow ${workflowId} executed successfully`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error executing workflow ${workflowId}:`, error);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,159 +1,105 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Workflow } from 'src/database';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
mapWorkflowAction,
|
||||
mapWorkflowFilter,
|
||||
mapWorkflow,
|
||||
WorkflowCreateDto,
|
||||
WorkflowResponseDto,
|
||||
WorkflowSearchDto,
|
||||
WorkflowTriggerResponseDto,
|
||||
WorkflowUpdateDto,
|
||||
} from 'src/dtos/workflow.dto';
|
||||
import { Permission, PluginContext, PluginTriggerType } from 'src/enum';
|
||||
import { pluginTriggers } from 'src/plugins';
|
||||
|
||||
import { Permission, WorkflowTrigger } 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';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowService extends BaseService {
|
||||
async create(auth: AuthDto, dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
|
||||
const context = this.getContextForTrigger(dto.triggerType);
|
||||
|
||||
const filterInserts = await this.validateAndMapFilters(dto.filters, context);
|
||||
const actionInserts = await this.validateAndMapActions(dto.actions, context);
|
||||
|
||||
const workflow = await this.workflowRepository.createWorkflow(
|
||||
{
|
||||
ownerId: auth.user.id,
|
||||
triggerType: dto.triggerType,
|
||||
name: dto.name,
|
||||
description: dto.description || '',
|
||||
enabled: dto.enabled ?? true,
|
||||
},
|
||||
filterInserts,
|
||||
actionInserts,
|
||||
);
|
||||
|
||||
return this.mapWorkflow(workflow);
|
||||
getTriggers(): WorkflowTriggerResponseDto[] {
|
||||
return getWorkflowTriggers();
|
||||
}
|
||||
|
||||
async getAll(auth: AuthDto): Promise<WorkflowResponseDto[]> {
|
||||
const workflows = await this.workflowRepository.getWorkflowsByOwner(auth.user.id);
|
||||
|
||||
return Promise.all(workflows.map((workflow) => this.mapWorkflow(workflow)));
|
||||
async search(auth: AuthDto, dto: WorkflowSearchDto): Promise<WorkflowResponseDto[]> {
|
||||
const workflows = await this.workflowRepository.search({ ...dto, ownerId: auth.user.id });
|
||||
return workflows.map((workflow) => mapWorkflow(workflow));
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<WorkflowResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.WorkflowRead, ids: [id] });
|
||||
const workflow = await this.findOrFail(id);
|
||||
return this.mapWorkflow(workflow);
|
||||
return mapWorkflow(workflow);
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
|
||||
const { steps: stepsDto, ...workflowDto } = dto;
|
||||
const steps = await this.resolveAndValidateSteps(stepsDto ?? [], workflowDto.trigger);
|
||||
|
||||
const workflow = await this.workflowRepository.create(
|
||||
{
|
||||
...workflowDto,
|
||||
ownerId: auth.user.id,
|
||||
},
|
||||
steps.map((step) => ({
|
||||
enabled: step.enabled ?? true,
|
||||
config: step.config,
|
||||
pluginMethodId: step.pluginMethod.id,
|
||||
})),
|
||||
);
|
||||
|
||||
return mapWorkflow({ ...workflow, steps: [] });
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: WorkflowUpdateDto): Promise<WorkflowResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.WorkflowUpdate, ids: [id] });
|
||||
|
||||
if (Object.values(dto).filter((prop) => prop !== undefined).length === 0) {
|
||||
throw new BadRequestException('No fields to update');
|
||||
}
|
||||
|
||||
const workflow = await this.findOrFail(id);
|
||||
const context = this.getContextForTrigger(dto.triggerType ?? workflow.triggerType);
|
||||
|
||||
const { filters, actions, ...workflowUpdate } = dto;
|
||||
const filterInserts = filters && (await this.validateAndMapFilters(filters, context));
|
||||
const actionInserts = actions && (await this.validateAndMapActions(actions, context));
|
||||
|
||||
const updatedWorkflow = await this.workflowRepository.updateWorkflow(
|
||||
const { steps: stepsDto, ...workflowDto } = dto;
|
||||
const current = await this.findOrFail(id);
|
||||
const steps = stepsDto ? await this.resolveAndValidateSteps(stepsDto, dto.trigger ?? current.trigger) : undefined;
|
||||
const workflow = await this.workflowRepository.update(
|
||||
id,
|
||||
workflowUpdate,
|
||||
filterInserts,
|
||||
actionInserts,
|
||||
workflowDto,
|
||||
steps?.map((step) => ({
|
||||
enabled: step.enabled ?? true,
|
||||
config: step.config,
|
||||
pluginMethodId: step.pluginMethod.id,
|
||||
})),
|
||||
);
|
||||
|
||||
return this.mapWorkflow(updatedWorkflow);
|
||||
return mapWorkflow(workflow);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.WorkflowDelete, ids: [id] });
|
||||
await this.workflowRepository.deleteWorkflow(id);
|
||||
await this.workflowRepository.delete(id);
|
||||
}
|
||||
|
||||
private async validateAndMapFilters(
|
||||
filters: Array<{ pluginFilterId: string; filterConfig?: any }>,
|
||||
requiredContext: PluginContext,
|
||||
) {
|
||||
for (const dto of filters) {
|
||||
const filter = await this.pluginRepository.getFilter(dto.pluginFilterId);
|
||||
if (!filter) {
|
||||
throw new BadRequestException(`Invalid filter ID: ${dto.pluginFilterId}`);
|
||||
private async resolveAndValidateSteps<T extends { method: string }>(steps: T[], trigger: WorkflowTrigger) {
|
||||
const methods = await this.pluginRepository.getForValidation();
|
||||
const results: Array<T & { pluginMethod: PluginMethodSearchResponse }> = [];
|
||||
|
||||
for (const step of steps) {
|
||||
const pluginMethod = resolveMethod(methods, step.method);
|
||||
if (!pluginMethod) {
|
||||
throw new BadRequestException(`Unknown method ${step.method}`);
|
||||
}
|
||||
|
||||
if (!filter.supportedContexts.includes(requiredContext)) {
|
||||
throw new BadRequestException(
|
||||
`Filter "${filter.title}" does not support ${requiredContext} context. Supported contexts: ${filter.supportedContexts.join(', ')}`,
|
||||
);
|
||||
if (!isMethodCompatible(pluginMethod, trigger)) {
|
||||
throw new BadRequestException(`Method "${step.method}" is incompatible with workflow trigger: "${trigger}"`);
|
||||
}
|
||||
|
||||
results.push({ ...step, pluginMethod });
|
||||
}
|
||||
|
||||
return filters.map((dto, index) => ({
|
||||
pluginFilterId: dto.pluginFilterId,
|
||||
filterConfig: dto.filterConfig || null,
|
||||
order: index,
|
||||
}));
|
||||
}
|
||||
// TODO make sure all steps can use a common WorkflowType
|
||||
|
||||
private async validateAndMapActions(
|
||||
actions: Array<{ pluginActionId: string; actionConfig?: any }>,
|
||||
requiredContext: PluginContext,
|
||||
) {
|
||||
for (const dto of actions) {
|
||||
const action = await this.pluginRepository.getAction(dto.pluginActionId);
|
||||
if (!action) {
|
||||
throw new BadRequestException(`Invalid action ID: ${dto.pluginActionId}`);
|
||||
}
|
||||
if (!action.supportedContexts.includes(requiredContext)) {
|
||||
throw new BadRequestException(
|
||||
`Action "${action.title}" does not support ${requiredContext} context. Supported contexts: ${action.supportedContexts.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return actions.map((dto, index) => ({
|
||||
pluginActionId: dto.pluginActionId,
|
||||
actionConfig: dto.actionConfig || null,
|
||||
order: index,
|
||||
}));
|
||||
}
|
||||
|
||||
private getContextForTrigger(type: PluginTriggerType) {
|
||||
const trigger = pluginTriggers.find((t) => t.type === type);
|
||||
if (!trigger) {
|
||||
throw new BadRequestException(`Invalid trigger type: ${type}`);
|
||||
}
|
||||
return trigger.contextType;
|
||||
return results;
|
||||
}
|
||||
|
||||
private async findOrFail(id: string) {
|
||||
const workflow = await this.workflowRepository.getWorkflow(id);
|
||||
const workflow = await this.workflowRepository.get(id);
|
||||
if (!workflow) {
|
||||
throw new BadRequestException('Workflow not found');
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private async mapWorkflow(workflow: Workflow): Promise<WorkflowResponseDto> {
|
||||
const filters = await this.workflowRepository.getFilters(workflow.id);
|
||||
const actions = await this.workflowRepository.getActions(workflow.id);
|
||||
|
||||
return {
|
||||
id: workflow.id,
|
||||
ownerId: workflow.ownerId,
|
||||
triggerType: workflow.triggerType,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
createdAt: workflow.createdAt.toISOString(),
|
||||
enabled: workflow.enabled,
|
||||
filters: filters.map((f) => mapWorkflowFilter(f)),
|
||||
actions: actions.map((a) => mapWorkflowAction(a)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { Asset, AssetFile } from 'src/database';
|
||||
import { AssetFile } from 'src/database';
|
||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEditActionItem } from 'src/dtos/editing.dto';
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ImageFormat,
|
||||
JobName,
|
||||
MemoryType,
|
||||
PluginTriggerType,
|
||||
QueueName,
|
||||
StorageFolder,
|
||||
SyncEntityType,
|
||||
@@ -20,6 +19,8 @@ import {
|
||||
TranscodeTarget,
|
||||
UserMetadataKey,
|
||||
VideoCodec,
|
||||
WorkflowTrigger,
|
||||
WorkflowType,
|
||||
} from 'src/enum';
|
||||
|
||||
export type DeepPartial<T> =
|
||||
@@ -259,22 +260,11 @@ export interface INotifyAlbumUpdateJob extends IEntityJob, IDelayedJob {
|
||||
recipientId: string;
|
||||
}
|
||||
|
||||
export interface WorkflowData {
|
||||
[PluginTriggerType.AssetCreate]: {
|
||||
userId: string;
|
||||
asset: Asset;
|
||||
};
|
||||
[PluginTriggerType.PersonRecognized]: {
|
||||
personId: string;
|
||||
assetId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IWorkflowJob<T extends PluginTriggerType = PluginTriggerType> {
|
||||
export type IWorkflowJob<T extends WorkflowType = WorkflowType> = {
|
||||
id: string;
|
||||
trigger: WorkflowTrigger;
|
||||
type: T;
|
||||
event: WorkflowData[T];
|
||||
}
|
||||
};
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
@@ -385,7 +375,7 @@ export type JobItem =
|
||||
| { name: JobName.Ocr; data: IEntityJob }
|
||||
|
||||
// Workflow
|
||||
| { name: JobName.WorkflowRun; data: IWorkflowJob }
|
||||
| { name: JobName.WorkflowAssetCreate; data: { workflowId: string; assetId: string } }
|
||||
|
||||
// Editor
|
||||
| { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob };
|
||||
@@ -548,3 +538,23 @@ export interface UserMetadata extends Record<UserMetadataKey, Record<string, any
|
||||
[UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string };
|
||||
[UserMetadataKey.Onboarding]: { isOnboarded: boolean };
|
||||
}
|
||||
|
||||
export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object';
|
||||
|
||||
export type JSONSchemaProperty = {
|
||||
type: JSONSchemaType;
|
||||
description?: string;
|
||||
default?: any;
|
||||
enum?: string[];
|
||||
array?: boolean;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
export interface JSONSchema {
|
||||
type: 'object';
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* JSON Schema types for plugin configuration schemas
|
||||
* Based on JSON Schema Draft 7
|
||||
*/
|
||||
|
||||
export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null';
|
||||
|
||||
export interface JSONSchemaProperty {
|
||||
type?: JSONSchemaType | JSONSchemaType[];
|
||||
description?: string;
|
||||
default?: any;
|
||||
enum?: any[];
|
||||
items?: JSONSchemaProperty;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean | JSONSchemaProperty;
|
||||
}
|
||||
|
||||
export interface JSONSchema {
|
||||
type: 'object';
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue };
|
||||
|
||||
export interface FilterConfig {
|
||||
[key: string]: ConfigValue;
|
||||
}
|
||||
|
||||
export interface ActionConfig {
|
||||
[key: string]: ConfigValue;
|
||||
}
|
||||
60
server/src/utils/workflow.ts
Normal file
60
server/src/utils/workflow.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { WorkflowTrigger, WorkflowType } from 'src/enum';
|
||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||
|
||||
export const triggerMap: Record<WorkflowTrigger, WorkflowType[]> = {
|
||||
[WorkflowTrigger.AssetCreate]: [WorkflowType.AssetV1],
|
||||
[WorkflowTrigger.PersonRecognized]: [WorkflowType.AssetV1],
|
||||
};
|
||||
|
||||
export const getWorkflowTriggers = () =>
|
||||
Object.entries(triggerMap).map(([trigger, types]) => ({ trigger: trigger as WorkflowTrigger, types }));
|
||||
|
||||
/** some types extend other types and have implied compatibility */
|
||||
const inferredMap: Record<WorkflowType, WorkflowType[]> = {
|
||||
[WorkflowType.AssetV1]: [],
|
||||
[WorkflowType.AssetPersonV1]: [WorkflowType.AssetV1],
|
||||
};
|
||||
|
||||
const withImpliedItems = (type: WorkflowType): WorkflowType[] => [type, ...inferredMap[type]];
|
||||
|
||||
export const isMethodCompatible = (pluginMethod: { types: WorkflowType[] }, trigger: WorkflowTrigger) => {
|
||||
const validTypes = triggerMap[trigger];
|
||||
const pluginCompatibility = pluginMethod.types.map((type) => withImpliedItems(type));
|
||||
for (const requested of validTypes) {
|
||||
for (const pluginCompatibilityGroup of pluginCompatibility) {
|
||||
if (pluginCompatibilityGroup.includes(requested)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const resolveMethod = (methods: PluginMethodSearchResponse[], method: string) => {
|
||||
const result = parseMethodString(method);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { pluginName, methodName } = result;
|
||||
|
||||
return methods.find((method) => method.pluginName === pluginName && method.name === methodName);
|
||||
};
|
||||
|
||||
export const asMethodString = (method: { pluginName: string; methodName: string }) => {
|
||||
return `${method.pluginName}#${method.methodName}`;
|
||||
};
|
||||
|
||||
const METHOD_REGEX = /^(?<name>[^@#\s]+)(?:@(?<version>[^#\s]*))?#(?<method>[^@#\s]+)$/;
|
||||
export const parseMethodString = (method: string) => {
|
||||
const matches = METHOD_REGEX.exec(method);
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginName = matches.groups?.name;
|
||||
const version = matches.groups?.version;
|
||||
const methodName = matches.groups?.method;
|
||||
return { pluginName, version, methodName };
|
||||
};
|
||||
@@ -410,7 +410,6 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case PluginRepository:
|
||||
case SearchRepository:
|
||||
case SessionRepository:
|
||||
case SharedLinkRepository:
|
||||
@@ -442,6 +441,10 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||
return new key(LoggingRepository.create());
|
||||
}
|
||||
|
||||
case PluginRepository: {
|
||||
return new key(db, LoggingRepository.create());
|
||||
}
|
||||
|
||||
case StorageRepository: {
|
||||
return new key(LoggingRepository.create());
|
||||
}
|
||||
@@ -473,7 +476,6 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case PluginRepository:
|
||||
case SessionRepository:
|
||||
case SyncRepository:
|
||||
case SyncCheckpointRepository:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PluginContext } 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';
|
||||
@@ -9,7 +9,8 @@ import { newMediumService } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let pluginRepo: PluginRepository;
|
||||
|
||||
const wasmBytes = Buffer.from('some-wasm-binary-data');
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PluginService, {
|
||||
@@ -21,7 +22,6 @@ const setup = (db?: Kysely<DB>) => {
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
pluginRepo = new PluginRepository(defaultDatabase);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -32,214 +32,202 @@ describe(PluginService.name, () => {
|
||||
describe('getAll', () => {
|
||||
it('should return empty array when no plugins exist', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
|
||||
expect(plugins).toEqual([]);
|
||||
await expect(sut.search({})).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('should return plugin without filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
it('should return plugin without methods', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
const result = await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'test-plugin',
|
||||
title: 'Test Plugin',
|
||||
description: 'A test plugin',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/test.wasm' },
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
[],
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
const plugins = await sut.search({});
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0]).toMatchObject({
|
||||
id: result.plugin.id,
|
||||
id: result.id,
|
||||
name: 'test-plugin',
|
||||
description: 'A test plugin',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
filters: [],
|
||||
actions: [],
|
||||
methods: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return plugin with filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
it('should return plugin with multiple methods', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
const result = await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'full-plugin',
|
||||
title: 'Full Plugin',
|
||||
description: 'A plugin with filters and actions',
|
||||
description: 'A plugin with multiple methods',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/full.wasm' },
|
||||
filters: [
|
||||
{
|
||||
methodName: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
methodName: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
[
|
||||
{
|
||||
name: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
name: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
const plugins = await sut.search({});
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0]).toMatchObject({
|
||||
id: result.plugin.id,
|
||||
id: result.id,
|
||||
name: 'full-plugin',
|
||||
filters: [
|
||||
methods: [
|
||||
{
|
||||
id: result.filters[0].id,
|
||||
pluginId: result.plugin.id,
|
||||
methodName: 'test-filter',
|
||||
id: result.methods[0].id,
|
||||
pluginId: result.id,
|
||||
name: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: result.actions[0].id,
|
||||
pluginId: result.plugin.id,
|
||||
methodName: 'test-action',
|
||||
id: result.methods[1].id,
|
||||
pluginId: result.id,
|
||||
name: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return multiple plugins with their respective filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
it('should return multiple plugins with their respective methods', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'plugin-1',
|
||||
title: 'Plugin 1',
|
||||
description: 'First plugin',
|
||||
author: 'Author 1',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/plugin1.wasm' },
|
||||
filters: [
|
||||
{
|
||||
methodName: 'filter-1',
|
||||
title: 'Filter 1',
|
||||
description: 'Filter for plugin 1',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
[
|
||||
{
|
||||
name: 'filter-1',
|
||||
title: 'Filter 1',
|
||||
description: 'Filter for plugin 1',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'plugin-2',
|
||||
title: 'Plugin 2',
|
||||
description: 'Second plugin',
|
||||
author: 'Author 2',
|
||||
version: '2.0.0',
|
||||
wasm: { path: '/path/to/plugin2.wasm' },
|
||||
actions: [
|
||||
{
|
||||
methodName: 'action-2',
|
||||
title: 'Action 2',
|
||||
description: 'Action for plugin 2',
|
||||
supportedContexts: [PluginContext.Album],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
[
|
||||
{
|
||||
name: 'action-2',
|
||||
title: 'Action 2',
|
||||
description: 'Action for plugin 2',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
const plugins = await sut.search({});
|
||||
|
||||
expect(plugins).toHaveLength(2);
|
||||
expect(plugins[0].name).toBe('plugin-1');
|
||||
expect(plugins[0].filters).toHaveLength(1);
|
||||
expect(plugins[0].actions).toHaveLength(0);
|
||||
expect(plugins[0].methods).toHaveLength(1);
|
||||
|
||||
expect(plugins[1].name).toBe('plugin-2');
|
||||
expect(plugins[1].filters).toHaveLength(0);
|
||||
expect(plugins[1].actions).toHaveLength(1);
|
||||
expect(plugins[1].methods).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle plugin with multiple filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
it('should handle plugin with multiple methods', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'multi-plugin',
|
||||
title: 'Multi Plugin',
|
||||
description: 'Plugin with multiple items',
|
||||
description: 'Plugin with multiple methods',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/multi.wasm' },
|
||||
filters: [
|
||||
{
|
||||
methodName: 'filter-a',
|
||||
title: 'Filter A',
|
||||
description: 'First filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
methodName: 'filter-b',
|
||||
title: 'Filter B',
|
||||
description: 'Second filter',
|
||||
supportedContexts: [PluginContext.Album],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
methodName: 'action-x',
|
||||
title: 'Action X',
|
||||
description: 'First action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
methodName: 'action-y',
|
||||
title: 'Action Y',
|
||||
description: 'Second action',
|
||||
supportedContexts: [PluginContext.Person],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
[
|
||||
{
|
||||
name: 'filter-a',
|
||||
title: 'Filter A',
|
||||
description: 'First filter',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
name: 'filter-b',
|
||||
title: 'Filter B',
|
||||
description: 'Second filter',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
name: 'action-x',
|
||||
title: 'Action X',
|
||||
description: 'First action',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
name: 'action-y',
|
||||
title: 'Action Y',
|
||||
description: 'Second action',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
const plugins = await sut.search({});
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0].filters).toHaveLength(2);
|
||||
expect(plugins[0].actions).toHaveLength(2);
|
||||
expect(plugins[0].methods).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,55 +238,51 @@ describe(PluginService.name, () => {
|
||||
await expect(sut.get('00000000-0000-0000-0000-000000000000')).rejects.toThrow('Plugin not found');
|
||||
});
|
||||
|
||||
it('should return single plugin with filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
it('should return single plugin with methods', async () => {
|
||||
const { ctx, sut } = setup();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
const result = await ctx.get(PluginRepository).create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'single-plugin',
|
||||
title: 'Single Plugin',
|
||||
description: 'A single plugin',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/single.wasm' },
|
||||
filters: [
|
||||
{
|
||||
methodName: 'single-filter',
|
||||
title: 'Single Filter',
|
||||
description: 'A single filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
methodName: 'single-action',
|
||||
title: 'Single Action',
|
||||
description: 'A single action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
const pluginResult = await sut.get(result.plugin.id);
|
||||
|
||||
expect(pluginResult).toMatchObject({
|
||||
id: result.plugin.id,
|
||||
name: 'single-plugin',
|
||||
filters: [
|
||||
[
|
||||
{
|
||||
id: result.filters[0].id,
|
||||
methodName: 'single-filter',
|
||||
name: 'single-filter',
|
||||
title: 'Single Filter',
|
||||
description: 'A single filter',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
name: 'single-action',
|
||||
title: 'Single Action',
|
||||
description: 'A single action',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
);
|
||||
|
||||
const pluginResult = await sut.get(result.id);
|
||||
|
||||
expect(pluginResult).toMatchObject({
|
||||
id: result.id,
|
||||
name: 'single-plugin',
|
||||
methods: [
|
||||
{
|
||||
id: result.actions[0].id,
|
||||
methodName: 'single-action',
|
||||
id: result.methods[0].id,
|
||||
name: 'single-filter',
|
||||
title: 'Single Filter',
|
||||
},
|
||||
{
|
||||
id: result.methods[1].id,
|
||||
name: 'single-action',
|
||||
title: 'Single Action',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PluginContext, PluginTriggerType } from 'src/enum';
|
||||
import { WorkflowTrigger, 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';
|
||||
@@ -20,53 +20,48 @@ const setup = (db?: Kysely<DB>) => {
|
||||
});
|
||||
};
|
||||
|
||||
const wasmBytes = Buffer.from('random-wasm-bytes');
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(WorkflowService.name, () => {
|
||||
let testPluginId: string;
|
||||
let testFilterId: string;
|
||||
let testActionId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a test plugin with filters and actions once for all tests
|
||||
const pluginRepo = new PluginRepository(defaultDatabase);
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
const { ctx } = setup();
|
||||
// Create a test plugin with methods and actions once for all tests
|
||||
const pluginRepo = ctx.get(PluginRepository);
|
||||
const result = await pluginRepo.create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'test-core-plugin',
|
||||
title: 'Test Core Plugin',
|
||||
description: 'A test core plugin for workflow tests',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: {
|
||||
path: '/test/path.wasm',
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
methodName: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
methodName: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
wasmBytes,
|
||||
},
|
||||
'/plugins/test-core-plugin',
|
||||
[
|
||||
{
|
||||
name: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
{
|
||||
name: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
types: [WorkflowType.AssetV1],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
testPluginId = result.plugin.id;
|
||||
testFilterId = result.filters[0].id;
|
||||
testActionId = result.actions[0].id;
|
||||
testPluginId = result.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -74,229 +69,48 @@ describe(WorkflowService.name, () => {
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a workflow without filters or actions', async () => {
|
||||
it('should create a workflow without methods or actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
expect(workflow).toMatchObject({
|
||||
id: expect.any(String),
|
||||
ownerId: user.id,
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a workflow with filters and actions', async () => {
|
||||
it('should create a workflow with methods and actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'test-workflow-with-relations',
|
||||
description: 'A test workflow with filters and actions',
|
||||
description: 'A test workflow with methods and actions',
|
||||
enabled: true,
|
||||
filters: [
|
||||
{
|
||||
pluginFilterId: testFilterId,
|
||||
filterConfig: { key: 'value' },
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
pluginActionId: testActionId,
|
||||
actionConfig: { action: 'test' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(workflow).toMatchObject({
|
||||
id: expect.any(String),
|
||||
ownerId: user.id,
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'test-workflow-with-relations',
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
expect(workflow.filters).toHaveLength(1);
|
||||
expect(workflow.filters[0]).toMatchObject({
|
||||
id: expect.any(String),
|
||||
workflowId: workflow.id,
|
||||
pluginFilterId: testFilterId,
|
||||
filterConfig: { key: 'value' },
|
||||
order: 0,
|
||||
});
|
||||
|
||||
expect(workflow.actions).toHaveLength(1);
|
||||
expect(workflow.actions[0]).toMatchObject({
|
||||
id: expect.any(String),
|
||||
workflowId: workflow.id,
|
||||
pluginActionId: testActionId,
|
||||
actionConfig: { action: 'test' },
|
||||
order: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when creating workflow with invalid filter', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-workflow',
|
||||
description: 'A workflow with invalid filter',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: factory.uuid(), filterConfig: { key: 'value' } }],
|
||||
actions: [],
|
||||
}),
|
||||
).rejects.toThrow('Invalid filter ID');
|
||||
});
|
||||
|
||||
it('should throw error when creating workflow with invalid action', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-workflow',
|
||||
description: 'A workflow with invalid action',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [{ pluginActionId: factory.uuid(), actionConfig: { action: 'test' } }],
|
||||
}),
|
||||
).rejects.toThrow('Invalid action ID');
|
||||
});
|
||||
|
||||
it('should throw error when filter does not support trigger context', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
// Create a plugin with a filter that only supports Album context
|
||||
const pluginRepo = new PluginRepository(defaultDatabase);
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
{
|
||||
name: 'album-only-plugin',
|
||||
title: 'Album Only Plugin',
|
||||
description: 'Plugin with album-only filter',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/test/album-plugin.wasm' },
|
||||
filters: [
|
||||
{
|
||||
methodName: 'album-filter',
|
||||
title: 'Album Filter',
|
||||
description: 'A filter that only works with albums',
|
||||
supportedContexts: [PluginContext.Album],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/plugins/test-core-plugin',
|
||||
);
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-context-workflow',
|
||||
description: 'A workflow with context mismatch',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: result.filters[0].id }],
|
||||
actions: [],
|
||||
}),
|
||||
).rejects.toThrow('does not support asset context');
|
||||
});
|
||||
|
||||
it('should throw error when action does not support trigger context', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
// Create a plugin with an action that only supports Person context
|
||||
const pluginRepo = new PluginRepository(defaultDatabase);
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
{
|
||||
name: 'person-only-plugin',
|
||||
title: 'Person Only Plugin',
|
||||
description: 'Plugin with person-only action',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/test/person-plugin.wasm' },
|
||||
actions: [
|
||||
{
|
||||
methodName: 'person-action',
|
||||
title: 'Person Action',
|
||||
description: 'An action that only works with persons',
|
||||
supportedContexts: [PluginContext.Person],
|
||||
schema: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/plugins/test-core-plugin',
|
||||
);
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-context-workflow',
|
||||
description: 'A workflow with context mismatch',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [{ pluginActionId: result.actions[0].id }],
|
||||
}),
|
||||
).rejects.toThrow('does not support asset context');
|
||||
});
|
||||
|
||||
it('should create workflow with multiple filters and actions in correct order', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'multi-step-workflow',
|
||||
description: 'A workflow with multiple filters and actions',
|
||||
enabled: true,
|
||||
filters: [
|
||||
{ pluginFilterId: testFilterId, filterConfig: { step: 1 } },
|
||||
{ pluginFilterId: testFilterId, filterConfig: { step: 2 } },
|
||||
],
|
||||
actions: [
|
||||
{ pluginActionId: testActionId, actionConfig: { step: 1 } },
|
||||
{ pluginActionId: testActionId, actionConfig: { step: 2 } },
|
||||
{ pluginActionId: testActionId, actionConfig: { step: 3 } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(workflow.filters).toHaveLength(2);
|
||||
expect(workflow.filters[0].order).toBe(0);
|
||||
expect(workflow.filters[0].filterConfig).toEqual({ step: 1 });
|
||||
expect(workflow.filters[1].order).toBe(1);
|
||||
expect(workflow.filters[1].filterConfig).toEqual({ step: 2 });
|
||||
|
||||
expect(workflow.actions).toHaveLength(3);
|
||||
expect(workflow.actions[0].order).toBe(0);
|
||||
expect(workflow.actions[1].order).toBe(1);
|
||||
expect(workflow.actions[2].order).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -307,24 +121,20 @@ describe(WorkflowService.name, () => {
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow1 = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'workflow-1',
|
||||
description: 'First workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const workflow2 = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'workflow-2',
|
||||
description: 'Second workflow',
|
||||
enabled: false,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const workflows = await sut.getAll(auth);
|
||||
const workflows = await sut.search(auth, {});
|
||||
|
||||
expect(workflows).toHaveLength(2);
|
||||
expect(workflows).toEqual(
|
||||
@@ -340,7 +150,7 @@ describe(WorkflowService.name, () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflows = await sut.getAll(auth);
|
||||
const workflows = await sut.search(auth, {});
|
||||
|
||||
expect(workflows).toEqual([]);
|
||||
});
|
||||
@@ -353,424 +163,15 @@ describe(WorkflowService.name, () => {
|
||||
const auth2 = factory.auth({ user: user2 });
|
||||
|
||||
await sut.create(auth1, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
name: 'user1-workflow',
|
||||
description: 'User 1 workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const user2Workflows = await sut.getAll(auth2);
|
||||
const user2Workflows = await sut.search(auth2, {});
|
||||
|
||||
expect(user2Workflows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return a specific workflow by id', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { key: 'value' } }],
|
||||
actions: [{ pluginActionId: testActionId, actionConfig: { action: 'test' } }],
|
||||
});
|
||||
|
||||
const workflow = await sut.get(auth, created.id);
|
||||
|
||||
expect(workflow).toMatchObject({
|
||||
id: created.id,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
});
|
||||
expect(workflow.filters).toHaveLength(1);
|
||||
expect(workflow.actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw error when workflow does not exist', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
await expect(sut.get(auth, '66da82df-e424-4bf4-b6f3-5d8e71620dae')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when user does not have access to workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth1 = factory.auth({ user: user1 });
|
||||
const auth2 = factory.auth({ user: user2 });
|
||||
|
||||
const workflow = await sut.create(auth1, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'private-workflow',
|
||||
description: 'Private workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(sut.get(auth2, workflow.id)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update workflow basic fields', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'original-workflow',
|
||||
description: 'Original description',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
name: 'updated-workflow',
|
||||
description: 'Updated description',
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
expect(updated).toMatchObject({
|
||||
id: created.id,
|
||||
name: 'updated-workflow',
|
||||
description: 'Updated description',
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update workflow filters', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { old: 'config' } }],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
filters: [
|
||||
{ pluginFilterId: testFilterId, filterConfig: { new: 'config' } },
|
||||
{ pluginFilterId: testFilterId, filterConfig: { second: 'filter' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(updated.filters).toHaveLength(2);
|
||||
expect(updated.filters[0].filterConfig).toEqual({ new: 'config' });
|
||||
expect(updated.filters[1].filterConfig).toEqual({ second: 'filter' });
|
||||
});
|
||||
|
||||
it('should update workflow actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [{ pluginActionId: testActionId, actionConfig: { old: 'config' } }],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
actions: [
|
||||
{ pluginActionId: testActionId, actionConfig: { new: 'config' } },
|
||||
{ pluginActionId: testActionId, actionConfig: { second: 'action' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(updated.actions).toHaveLength(2);
|
||||
expect(updated.actions[0].actionConfig).toEqual({ new: 'config' });
|
||||
expect(updated.actions[1].actionConfig).toEqual({ second: 'action' });
|
||||
});
|
||||
|
||||
it('should clear filters when updated with empty array', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { key: 'value' } }],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
filters: [],
|
||||
});
|
||||
|
||||
expect(updated.filters).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should throw error when no fields to update', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(sut.update(auth, created.id, {})).rejects.toThrow('No fields to update');
|
||||
});
|
||||
|
||||
it('should throw error when updating non-existent workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
await expect(sut.update(auth, factory.uuid(), { name: 'updated-name' })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when user does not have access to update workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth1 = factory.auth({ user: user1 });
|
||||
const auth2 = factory.auth({ user: user2 });
|
||||
|
||||
const workflow = await sut.create(auth1, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'private-workflow',
|
||||
description: 'Private',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
sut.update(auth2, workflow.id, {
|
||||
name: 'hacked-workflow',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when updating with invalid filter', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
sut.update(auth, created.id, {
|
||||
filters: [{ pluginFilterId: factory.uuid(), filterConfig: {} }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when updating with invalid action', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
sut.update(auth, created.id, { actions: [{ pluginActionId: factory.uuid(), actionConfig: {} }] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should update trigger type', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.PersonRecognized,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await sut.update(auth, created.id, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
});
|
||||
|
||||
const fetched = await sut.get(auth, created.id);
|
||||
expect(fetched.triggerType).toBe(PluginTriggerType.AssetCreate);
|
||||
});
|
||||
|
||||
it('should add filters', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await sut.update(auth, created.id, {
|
||||
filters: [
|
||||
{ pluginFilterId: testFilterId, filterConfig: { first: true } },
|
||||
{ pluginFilterId: testFilterId, filterConfig: { second: true } },
|
||||
],
|
||||
});
|
||||
|
||||
const fetched = await sut.get(auth, created.id);
|
||||
expect(fetched.filters).toHaveLength(2);
|
||||
expect(fetched.filters[0].filterConfig).toEqual({ first: true });
|
||||
expect(fetched.filters[1].filterConfig).toEqual({ second: true });
|
||||
});
|
||||
|
||||
it('should replace existing filters', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { original: true } }],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await sut.update(auth, created.id, {
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { replaced: true } }],
|
||||
});
|
||||
|
||||
const fetched = await sut.get(auth, created.id);
|
||||
expect(fetched.filters).toHaveLength(1);
|
||||
expect(fetched.filters[0].filterConfig).toEqual({ replaced: true });
|
||||
});
|
||||
|
||||
it('should remove existing filters', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: { toRemove: true } }],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await sut.update(auth, created.id, {
|
||||
filters: [],
|
||||
});
|
||||
|
||||
const fetched = await sut.get(auth, created.id);
|
||||
expect(fetched.filters).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await sut.delete(auth, workflow.id);
|
||||
|
||||
await expect(sut.get(auth, workflow.id)).rejects.toThrow('Not found or no workflow.read access');
|
||||
});
|
||||
|
||||
it('should delete workflow with filters and actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ pluginFilterId: testFilterId, filterConfig: {} }],
|
||||
actions: [{ pluginActionId: testActionId, actionConfig: {} }],
|
||||
});
|
||||
|
||||
await sut.delete(auth, workflow.id);
|
||||
|
||||
await expect(sut.get(auth, workflow.id)).rejects.toThrow('Not found or no workflow.read access');
|
||||
});
|
||||
|
||||
it('should throw error when deleting non-existent workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
await expect(sut.delete(auth, factory.uuid())).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when user does not have access to delete workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth1 = factory.auth({ user: user1 });
|
||||
const auth2 = factory.auth({ user: user2 });
|
||||
|
||||
const workflow = await sut.create(auth1, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'private-workflow',
|
||||
description: 'Private',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await expect(sut.delete(auth2, workflow.id)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
213
server/test/medium/specs/workflow/workflow-core-plugin.spec.ts
Normal file
213
server/test/medium/specs/workflow/workflow-core-plugin.spec.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { WorkflowStepConfig } from '@immich/plugin-sdk';
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetVisibility, WorkflowTrigger } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
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 { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { WorkflowExecutionService } from 'src/services/workflow-execution.service';
|
||||
import { resolveMethod } from 'src/utils/workflow';
|
||||
import { MediumTestContext } from 'test/medium.factory';
|
||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let initialized = false;
|
||||
|
||||
class WorkflowTestContext extends MediumTestContext<WorkflowExecutionService> {
|
||||
constructor(database: Kysely<DB>) {
|
||||
super(WorkflowExecutionService, {
|
||||
database,
|
||||
real: [
|
||||
AccessRepository,
|
||||
AlbumRepository,
|
||||
AssetRepository,
|
||||
CryptoRepository,
|
||||
DatabaseRepository,
|
||||
LoggingRepository,
|
||||
StorageRepository,
|
||||
PluginRepository,
|
||||
WorkflowRepository,
|
||||
],
|
||||
mock: [ConfigRepository],
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mockData = mockEnvData({});
|
||||
mockData.resourcePaths.corePlugin = '../packages/plugin-core';
|
||||
mockData.plugins.external.allow = false;
|
||||
this.getMock(ConfigRepository).getEnv.mockReturnValue(mockData);
|
||||
|
||||
await this.sut.onPluginSync();
|
||||
await this.sut.onPluginLoad();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowTemplate = {
|
||||
ownerId: string;
|
||||
trigger: WorkflowTrigger;
|
||||
steps: WorkflowTemplateStep[];
|
||||
};
|
||||
|
||||
type WorkflowTemplateStep = {
|
||||
method: string;
|
||||
config?: WorkflowStepConfig;
|
||||
};
|
||||
|
||||
const createWorkflow = async (template: WorkflowTemplate) => {
|
||||
const workflowRepo = ctx.get(WorkflowRepository);
|
||||
const pluginRepo = ctx.get(PluginRepository);
|
||||
|
||||
const methods = await pluginRepo.getForValidation();
|
||||
const steps = template.steps.map((step) => {
|
||||
const pluginMethod = resolveMethod(methods, step.method);
|
||||
if (!pluginMethod) {
|
||||
throw new Error(`Plugin method not found: ${step.method}`);
|
||||
}
|
||||
|
||||
return { ...step, pluginMethod };
|
||||
});
|
||||
|
||||
return workflowRepo.create(
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Test workflow',
|
||||
description: 'A workflow to test the core plugin',
|
||||
ownerId: template.ownerId,
|
||||
trigger: template.trigger,
|
||||
},
|
||||
steps.map((step) => ({
|
||||
enabled: true,
|
||||
pluginMethodId: step.pluginMethod.id,
|
||||
config: step.config,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
let ctx: WorkflowTestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
const db = await getKyselyDB();
|
||||
ctx = new WorkflowTestContext(db);
|
||||
await ctx.init();
|
||||
});
|
||||
|
||||
describe('core plugin', () => {
|
||||
describe('assetArchive', () => {
|
||||
it('should archive an asset', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [{ method: 'immich-plugin-core#assetArchive' }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Archive,
|
||||
});
|
||||
});
|
||||
|
||||
it('should unarchive an asset', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Archive });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [{ method: 'immich-plugin-core#assetArchive', config: { inverse: true } }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Timeline,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('assetFavoriteAsset', () => {
|
||||
it('should favorite an asset', async () => {
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [{ method: 'immich-plugin-core#assetFavorite' }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
|
||||
it('should unfavorite an asset', 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#assetFavorite', config: { inverse: true } }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('albumAddAssets', () => {
|
||||
it('should add an asset to an album', 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#albumAddAssets', config: { albumId: album.id } }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
const assetIds = await ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id]);
|
||||
expect([...assetIds]).toEqual([asset.id]);
|
||||
});
|
||||
|
||||
it('should require album access', async () => {
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({ ownerId: user1.id, isFavorite: true });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||
|
||||
const workflow = await createWorkflow({
|
||||
ownerId: user1.id,
|
||||
trigger: WorkflowTrigger.AssetCreate,
|
||||
steps: [{ method: 'immich-plugin-core#albumAddAssets', config: { albumId: album.id } }],
|
||||
});
|
||||
|
||||
await ctx.sut.handleWorkflowAssetCreate({ workflowId: workflow.id, assetId: asset.id });
|
||||
|
||||
const assetIds = await ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id]);
|
||||
expect([...assetIds]).not.toContain(asset.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user