mirror of
https://github.com/immich-app/immich.git
synced 2025-12-15 06:24:23 +03:00
feat: workflow foundation (#23621)
* feat: plugins * feat: table definition * feat: type and migration * feat: add repositories * feat: validate manifest with class-validator and load manifest info to database * feat: workflow/plugin controller/service layer * feat: implement workflow logic * feat: make trigger static * feat: dynamical instantiate plugin instances * fix: access control and helper script * feat: it works * chore: simplify * refactor: refactor and use queue for workflow execution * refactor: remove unsused property in plugin-schema * build wasm in prod * feat: plugin loader in transaction * fix: docker build arm64 * generated files * shell check * fix tests * fix: waiting for migration to finish before loading plugin * remove context reassignment * feat: use mise to manage extism tools (#23760) * pr feedback * refactor: create workflow now including create filters and actions * feat: workflow medium tests * fix: broken medium test * feat: medium tests * chore: unify workflow job * sign user id with jwt * chore: query plugin with filters and action * chore: read manifest in repository * chore: load manifest from server configs * merge main * feat: endpoint documentation * pr feedback * load plugin from absolute path * refactor:handle trigger * throw error and return early * pr feedback * unify plugin services * fix: plugins code * clean up * remove triggerConfig * clean up * displayName and methodName --------- Co-authored-by: Jason Rasmussen <jason@rasm.me> Co-authored-by: bo0tzz <git@bo0tzz.me>
This commit is contained in:
@@ -41,6 +41,7 @@ services:
|
||||
- app-node_modules:/usr/src/app/node_modules
|
||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
||||
- coverage:/usr/src/app/web/coverage
|
||||
- ../plugins:/build/corePlugin
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -2181,6 +2181,7 @@
|
||||
"welcome": "Welcome",
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"workflow": "Workflow",
|
||||
"wrong_pin_code": "Wrong PIN code",
|
||||
"year": "Year",
|
||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||
|
||||
19
mobile/openapi/README.md
generated
19
mobile/openapi/README.md
generated
@@ -194,6 +194,8 @@ Class | Method | HTTP request | Description
|
||||
*PeopleApi* | [**reassignFaces**](doc//PeopleApi.md#reassignfaces) | **PUT** /people/{id}/reassign | Reassign faces
|
||||
*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* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins
|
||||
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city
|
||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data
|
||||
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | Retrieve search suggestions
|
||||
@@ -295,6 +297,11 @@ Class | Method | HTTP request | Description
|
||||
*UsersAdminApi* | [**updateUserPreferencesAdmin**](doc//UsersAdminApi.md#updateuserpreferencesadmin) | **PUT** /admin/users/{id}/preferences | Update user preferences
|
||||
*ViewsApi* | [**getAssetsByOriginalPath**](doc//ViewsApi.md#getassetsbyoriginalpath) | **GET** /view/folder | Retrieve assets by original path
|
||||
*ViewsApi* | [**getUniqueOriginalPaths**](doc//ViewsApi.md#getuniqueoriginalpaths) | **GET** /view/folder/unique-paths | Retrieve unique paths
|
||||
*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* | [**updateWorkflow**](doc//WorkflowsApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
|
||||
|
||||
|
||||
## Documentation For Models
|
||||
@@ -444,6 +451,11 @@ Class | Method | HTTP request | Description
|
||||
- [PinCodeResetDto](doc//PinCodeResetDto.md)
|
||||
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||
- [PluginActionResponseDto](doc//PluginActionResponseDto.md)
|
||||
- [PluginContext](doc//PluginContext.md)
|
||||
- [PluginFilterResponseDto](doc//PluginFilterResponseDto.md)
|
||||
- [PluginResponseDto](doc//PluginResponseDto.md)
|
||||
- [PluginTriggerType](doc//PluginTriggerType.md)
|
||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||
- [QueueCommand](doc//QueueCommand.md)
|
||||
@@ -603,6 +615,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)
|
||||
- [WorkflowUpdateDto](doc//WorkflowUpdateDto.md)
|
||||
|
||||
|
||||
## Documentation For Authorization
|
||||
|
||||
14
mobile/openapi/lib/api.dart
generated
14
mobile/openapi/lib/api.dart
generated
@@ -48,6 +48,7 @@ part 'api/notifications_api.dart';
|
||||
part 'api/notifications_admin_api.dart';
|
||||
part 'api/partners_api.dart';
|
||||
part 'api/people_api.dart';
|
||||
part 'api/plugins_api.dart';
|
||||
part 'api/search_api.dart';
|
||||
part 'api/server_api.dart';
|
||||
part 'api/sessions_api.dart';
|
||||
@@ -62,6 +63,7 @@ part 'api/trash_api.dart';
|
||||
part 'api/users_api.dart';
|
||||
part 'api/users_admin_api.dart';
|
||||
part 'api/views_api.dart';
|
||||
part 'api/workflows_api.dart';
|
||||
|
||||
part 'model/api_key_create_dto.dart';
|
||||
part 'model/api_key_create_response_dto.dart';
|
||||
@@ -208,6 +210,11 @@ 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.dart';
|
||||
part 'model/plugin_filter_response_dto.dart';
|
||||
part 'model/plugin_response_dto.dart';
|
||||
part 'model/plugin_trigger_type.dart';
|
||||
part 'model/purchase_response.dart';
|
||||
part 'model/purchase_update.dart';
|
||||
part 'model/queue_command.dart';
|
||||
@@ -367,6 +374,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_update_dto.dart';
|
||||
|
||||
|
||||
/// An [ApiClient] instance that uses the default values obtained from
|
||||
|
||||
126
mobile/openapi/lib/api/plugins_api.dart
generated
Normal file
126
mobile/openapi/lib/api/plugins_api.dart
generated
Normal file
@@ -0,0 +1,126 @@
|
||||
//
|
||||
// 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 PluginsApi {
|
||||
PluginsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Retrieve a plugin
|
||||
///
|
||||
/// Retrieve information about a specific plugin by its ID.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getPluginWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/plugins/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieve a plugin
|
||||
///
|
||||
/// Retrieve information about a specific plugin by its ID.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<PluginResponseDto?> getPlugin(String id,) async {
|
||||
final response = await getPluginWithHttpInfo(id,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PluginResponseDto',) as PluginResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all plugins
|
||||
///
|
||||
/// Retrieve a list of plugins available to the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getPluginsWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/plugins';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// List all plugins
|
||||
///
|
||||
/// Retrieve a list of plugins available to the authenticated user.
|
||||
Future<List<PluginResponseDto>?> getPlugins() async {
|
||||
final response = await getPluginsWithHttpInfo();
|
||||
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<PluginResponseDto>') as List)
|
||||
.cast<PluginResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
292
mobile/openapi/lib/api/workflows_api.dart
generated
Normal file
292
mobile/openapi/lib/api/workflows_api.dart
generated
Normal file
@@ -0,0 +1,292 @@
|
||||
//
|
||||
// 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 WorkflowsApi {
|
||||
WorkflowsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Create a workflow
|
||||
///
|
||||
/// Create a new workflow, the workflow can also be created with empty filters and actions.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [WorkflowCreateDto] workflowCreateDto (required):
|
||||
Future<Response> createWorkflowWithHttpInfo(WorkflowCreateDto workflowCreateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = workflowCreateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a workflow
|
||||
///
|
||||
/// Create a new workflow, the workflow can also be created with empty filters and actions.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [WorkflowCreateDto] workflowCreateDto (required):
|
||||
Future<WorkflowResponseDto?> createWorkflow(WorkflowCreateDto workflowCreateDto,) async {
|
||||
final response = await createWorkflowWithHttpInfo(workflowCreateDto,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'WorkflowResponseDto',) as WorkflowResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Delete a workflow
|
||||
///
|
||||
/// Delete a workflow by its ID.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> deleteWorkflowWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete a workflow
|
||||
///
|
||||
/// Delete a workflow by its ID.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> deleteWorkflow(String id,) async {
|
||||
final response = await deleteWorkflowWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a workflow
|
||||
///
|
||||
/// Retrieve information about a specific workflow by its ID.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getWorkflowWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieve a workflow
|
||||
///
|
||||
/// Retrieve information about a specific workflow by its ID.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<WorkflowResponseDto?> getWorkflow(String id,) async {
|
||||
final response = await getWorkflowWithHttpInfo(id,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'WorkflowResponseDto',) as WorkflowResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all workflows
|
||||
///
|
||||
/// Retrieve a list of workflows available to the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getWorkflowsWithHttpInfo() 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>{};
|
||||
|
||||
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.
|
||||
Future<List<WorkflowResponseDto>?> getWorkflows() async {
|
||||
final response = await getWorkflowsWithHttpInfo();
|
||||
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<WorkflowResponseDto>') as List)
|
||||
.cast<WorkflowResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Update a workflow
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [WorkflowUpdateDto] workflowUpdateDto (required):
|
||||
Future<Response> updateWorkflowWithHttpInfo(String id, WorkflowUpdateDto workflowUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = workflowUpdateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Update a workflow
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [WorkflowUpdateDto] workflowUpdateDto (required):
|
||||
Future<WorkflowResponseDto?> updateWorkflow(String id, WorkflowUpdateDto workflowUpdateDto,) async {
|
||||
final response = await updateWorkflowWithHttpInfo(id, workflowUpdateDto,);
|
||||
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) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'WorkflowResponseDto',) as WorkflowResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
24
mobile/openapi/lib/api_client.dart
generated
24
mobile/openapi/lib/api_client.dart
generated
@@ -472,6 +472,16 @@ class ApiClient {
|
||||
return PinCodeSetupDto.fromJson(value);
|
||||
case 'PlacesResponseDto':
|
||||
return PlacesResponseDto.fromJson(value);
|
||||
case 'PluginActionResponseDto':
|
||||
return PluginActionResponseDto.fromJson(value);
|
||||
case 'PluginContext':
|
||||
return PluginContextTypeTransformer().decode(value);
|
||||
case 'PluginFilterResponseDto':
|
||||
return PluginFilterResponseDto.fromJson(value);
|
||||
case 'PluginResponseDto':
|
||||
return PluginResponseDto.fromJson(value);
|
||||
case 'PluginTriggerType':
|
||||
return PluginTriggerTypeTypeTransformer().decode(value);
|
||||
case 'PurchaseResponse':
|
||||
return PurchaseResponse.fromJson(value);
|
||||
case 'PurchaseUpdate':
|
||||
@@ -790,6 +800,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 'WorkflowUpdateDto':
|
||||
return WorkflowUpdateDto.fromJson(value);
|
||||
default:
|
||||
dynamic match;
|
||||
if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) {
|
||||
|
||||
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
@@ -121,6 +121,12 @@ String parameterToString(dynamic value) {
|
||||
if (value is Permission) {
|
||||
return PermissionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PluginContext) {
|
||||
return PluginContextTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is PluginTriggerType) {
|
||||
return PluginTriggerTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueCommand) {
|
||||
return QueueCommandTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
24
mobile/openapi/lib/model/permission.dart
generated
24
mobile/openapi/lib/model/permission.dart
generated
@@ -98,6 +98,10 @@ class Permission {
|
||||
static const pinCodePeriodCreate = Permission._(r'pinCode.create');
|
||||
static const pinCodePeriodUpdate = Permission._(r'pinCode.update');
|
||||
static const pinCodePeriodDelete = Permission._(r'pinCode.delete');
|
||||
static const pluginPeriodCreate = Permission._(r'plugin.create');
|
||||
static const pluginPeriodRead = Permission._(r'plugin.read');
|
||||
static const pluginPeriodUpdate = Permission._(r'plugin.update');
|
||||
static const pluginPeriodDelete = Permission._(r'plugin.delete');
|
||||
static const serverPeriodAbout = Permission._(r'server.about');
|
||||
static const serverPeriodApkLinks = Permission._(r'server.apkLinks');
|
||||
static const serverPeriodStorage = Permission._(r'server.storage');
|
||||
@@ -147,6 +151,10 @@ class Permission {
|
||||
static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read');
|
||||
static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update');
|
||||
static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete');
|
||||
static const workflowPeriodCreate = Permission._(r'workflow.create');
|
||||
static const workflowPeriodRead = Permission._(r'workflow.read');
|
||||
static const workflowPeriodUpdate = Permission._(r'workflow.update');
|
||||
static const workflowPeriodDelete = Permission._(r'workflow.delete');
|
||||
static const adminUserPeriodCreate = Permission._(r'adminUser.create');
|
||||
static const adminUserPeriodRead = Permission._(r'adminUser.read');
|
||||
static const adminUserPeriodUpdate = Permission._(r'adminUser.update');
|
||||
@@ -231,6 +239,10 @@ class Permission {
|
||||
pinCodePeriodCreate,
|
||||
pinCodePeriodUpdate,
|
||||
pinCodePeriodDelete,
|
||||
pluginPeriodCreate,
|
||||
pluginPeriodRead,
|
||||
pluginPeriodUpdate,
|
||||
pluginPeriodDelete,
|
||||
serverPeriodAbout,
|
||||
serverPeriodApkLinks,
|
||||
serverPeriodStorage,
|
||||
@@ -280,6 +292,10 @@ class Permission {
|
||||
userProfileImagePeriodRead,
|
||||
userProfileImagePeriodUpdate,
|
||||
userProfileImagePeriodDelete,
|
||||
workflowPeriodCreate,
|
||||
workflowPeriodRead,
|
||||
workflowPeriodUpdate,
|
||||
workflowPeriodDelete,
|
||||
adminUserPeriodCreate,
|
||||
adminUserPeriodRead,
|
||||
adminUserPeriodUpdate,
|
||||
@@ -399,6 +415,10 @@ class PermissionTypeTransformer {
|
||||
case r'pinCode.create': return Permission.pinCodePeriodCreate;
|
||||
case r'pinCode.update': return Permission.pinCodePeriodUpdate;
|
||||
case r'pinCode.delete': return Permission.pinCodePeriodDelete;
|
||||
case r'plugin.create': return Permission.pluginPeriodCreate;
|
||||
case r'plugin.read': return Permission.pluginPeriodRead;
|
||||
case r'plugin.update': return Permission.pluginPeriodUpdate;
|
||||
case r'plugin.delete': return Permission.pluginPeriodDelete;
|
||||
case r'server.about': return Permission.serverPeriodAbout;
|
||||
case r'server.apkLinks': return Permission.serverPeriodApkLinks;
|
||||
case r'server.storage': return Permission.serverPeriodStorage;
|
||||
@@ -448,6 +468,10 @@ class PermissionTypeTransformer {
|
||||
case r'userProfileImage.read': return Permission.userProfileImagePeriodRead;
|
||||
case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate;
|
||||
case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete;
|
||||
case r'workflow.create': return Permission.workflowPeriodCreate;
|
||||
case r'workflow.read': return Permission.workflowPeriodRead;
|
||||
case r'workflow.update': return Permission.workflowPeriodUpdate;
|
||||
case r'workflow.delete': return Permission.workflowPeriodDelete;
|
||||
case r'adminUser.create': return Permission.adminUserPeriodCreate;
|
||||
case r'adminUser.read': return Permission.adminUserPeriodRead;
|
||||
case r'adminUser.update': return Permission.adminUserPeriodUpdate;
|
||||
|
||||
151
mobile/openapi/lib/model/plugin_action_response_dto.dart
generated
Normal file
151
mobile/openapi/lib/model/plugin_action_response_dto.dart
generated
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// 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 PluginActionResponseDto {
|
||||
/// Returns a new [PluginActionResponseDto] instance.
|
||||
PluginActionResponseDto({
|
||||
required this.description,
|
||||
required this.id,
|
||||
required this.methodName,
|
||||
required this.pluginId,
|
||||
required this.schema,
|
||||
this.supportedContexts = const [],
|
||||
required this.title,
|
||||
});
|
||||
|
||||
String description;
|
||||
|
||||
String id;
|
||||
|
||||
String methodName;
|
||||
|
||||
String pluginId;
|
||||
|
||||
Object? schema;
|
||||
|
||||
List<PluginContext> supportedContexts;
|
||||
|
||||
String title;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginActionResponseDto &&
|
||||
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() => 'PluginActionResponseDto[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 [PluginActionResponseDto] 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");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PluginActionResponseDto(
|
||||
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: PluginContext.listFromJson(json[r'supportedContexts']),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PluginActionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginActionResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginActionResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PluginActionResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginActionResponseDto>{};
|
||||
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);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
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>>{};
|
||||
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,);
|
||||
}
|
||||
}
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
88
mobile/openapi/lib/model/plugin_context.dart
generated
Normal file
88
mobile/openapi/lib/model/plugin_context.dart
generated
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// 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 PluginContext {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const PluginContext._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const asset = PluginContext._(r'asset');
|
||||
static const album = PluginContext._(r'album');
|
||||
static const person = PluginContext._(r'person');
|
||||
|
||||
/// List of all possible values in this [enum][PluginContext].
|
||||
static const values = <PluginContext>[
|
||||
asset,
|
||||
album,
|
||||
person,
|
||||
];
|
||||
|
||||
static PluginContext? fromJson(dynamic value) => PluginContextTypeTransformer().decode(value);
|
||||
|
||||
static List<PluginContext> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginContext>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginContext.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [PluginContext] to String,
|
||||
/// and [decode] dynamic data back to [PluginContext].
|
||||
class PluginContextTypeTransformer {
|
||||
factory PluginContextTypeTransformer() => _instance ??= const PluginContextTypeTransformer._();
|
||||
|
||||
const PluginContextTypeTransformer._();
|
||||
|
||||
String encode(PluginContext data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a PluginContext.
|
||||
///
|
||||
/// 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.
|
||||
PluginContext? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'asset': return PluginContext.asset;
|
||||
case r'album': return PluginContext.album;
|
||||
case r'person': return PluginContext.person;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [PluginContextTypeTransformer] instance.
|
||||
static PluginContextTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
151
mobile/openapi/lib/model/plugin_filter_response_dto.dart
generated
Normal file
151
mobile/openapi/lib/model/plugin_filter_response_dto.dart
generated
Normal file
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// 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,
|
||||
});
|
||||
|
||||
String description;
|
||||
|
||||
String id;
|
||||
|
||||
String methodName;
|
||||
|
||||
String pluginId;
|
||||
|
||||
Object? schema;
|
||||
|
||||
List<PluginContext> supportedContexts;
|
||||
|
||||
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: PluginContext.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',
|
||||
};
|
||||
}
|
||||
|
||||
171
mobile/openapi/lib/model/plugin_response_dto.dart
generated
Normal file
171
mobile/openapi/lib/model/plugin_response_dto.dart
generated
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// 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 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,
|
||||
required this.name,
|
||||
required this.title,
|
||||
required this.updatedAt,
|
||||
required this.version,
|
||||
});
|
||||
|
||||
List<PluginActionResponseDto> actions;
|
||||
|
||||
String author;
|
||||
|
||||
String createdAt;
|
||||
|
||||
String description;
|
||||
|
||||
List<PluginFilterResponseDto> filters;
|
||||
|
||||
String id;
|
||||
|
||||
String name;
|
||||
|
||||
String title;
|
||||
|
||||
String updatedAt;
|
||||
|
||||
String version;
|
||||
|
||||
@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 &&
|
||||
other.name == name &&
|
||||
other.title == title &&
|
||||
other.updatedAt == updatedAt &&
|
||||
other.version == version;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(author.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(id.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]';
|
||||
|
||||
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'name'] = this.name;
|
||||
json[r'title'] = this.title;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
json[r'version'] = this.version;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PluginResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PluginResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PluginResponseDto");
|
||||
if (value is Map) {
|
||||
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')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
version: mapValueOfType<String>(json, r'version')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PluginResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PluginResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PluginResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PluginResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PluginResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PluginResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PluginResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PluginResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PluginResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PluginResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actions',
|
||||
'author',
|
||||
'createdAt',
|
||||
'description',
|
||||
'filters',
|
||||
'id',
|
||||
'name',
|
||||
'title',
|
||||
'updatedAt',
|
||||
'version',
|
||||
};
|
||||
}
|
||||
|
||||
85
mobile/openapi/lib/model/plugin_trigger_type.dart
generated
Normal file
85
mobile/openapi/lib/model/plugin_trigger_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;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
3
mobile/openapi/lib/model/queue_name.dart
generated
3
mobile/openapi/lib/model/queue_name.dart
generated
@@ -39,6 +39,7 @@ class QueueName {
|
||||
static const notifications = QueueName._(r'notifications');
|
||||
static const backupDatabase = QueueName._(r'backupDatabase');
|
||||
static const ocr = QueueName._(r'ocr');
|
||||
static const workflow = QueueName._(r'workflow');
|
||||
|
||||
/// List of all possible values in this [enum][QueueName].
|
||||
static const values = <QueueName>[
|
||||
@@ -58,6 +59,7 @@ class QueueName {
|
||||
notifications,
|
||||
backupDatabase,
|
||||
ocr,
|
||||
workflow,
|
||||
];
|
||||
|
||||
static QueueName? fromJson(dynamic value) => QueueNameTypeTransformer().decode(value);
|
||||
@@ -112,6 +114,7 @@ class QueueNameTypeTransformer {
|
||||
case r'notifications': return QueueName.notifications;
|
||||
case r'backupDatabase': return QueueName.backupDatabase;
|
||||
case r'ocr': return QueueName.ocr;
|
||||
case r'workflow': return QueueName.workflow;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
14
mobile/openapi/lib/model/queues_response_dto.dart
generated
14
mobile/openapi/lib/model/queues_response_dto.dart
generated
@@ -29,6 +29,7 @@ class QueuesResponseDto {
|
||||
required this.storageTemplateMigration,
|
||||
required this.thumbnailGeneration,
|
||||
required this.videoConversion,
|
||||
required this.workflow,
|
||||
});
|
||||
|
||||
QueueResponseDto backgroundTask;
|
||||
@@ -63,6 +64,8 @@ class QueuesResponseDto {
|
||||
|
||||
QueueResponseDto videoConversion;
|
||||
|
||||
QueueResponseDto workflow;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto &&
|
||||
other.backgroundTask == backgroundTask &&
|
||||
@@ -80,7 +83,8 @@ class QueuesResponseDto {
|
||||
other.smartSearch == smartSearch &&
|
||||
other.storageTemplateMigration == storageTemplateMigration &&
|
||||
other.thumbnailGeneration == thumbnailGeneration &&
|
||||
other.videoConversion == videoConversion;
|
||||
other.videoConversion == videoConversion &&
|
||||
other.workflow == workflow;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -100,10 +104,11 @@ class QueuesResponseDto {
|
||||
(smartSearch.hashCode) +
|
||||
(storageTemplateMigration.hashCode) +
|
||||
(thumbnailGeneration.hashCode) +
|
||||
(videoConversion.hashCode);
|
||||
(videoConversion.hashCode) +
|
||||
(workflow.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -123,6 +128,7 @@ class QueuesResponseDto {
|
||||
json[r'storageTemplateMigration'] = this.storageTemplateMigration;
|
||||
json[r'thumbnailGeneration'] = this.thumbnailGeneration;
|
||||
json[r'videoConversion'] = this.videoConversion;
|
||||
json[r'workflow'] = this.workflow;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -151,6 +157,7 @@ class QueuesResponseDto {
|
||||
storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!,
|
||||
thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!,
|
||||
workflow: QueueResponseDto.fromJson(json[r'workflow'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -214,6 +221,7 @@ class QueuesResponseDto {
|
||||
'storageTemplateMigration',
|
||||
'thumbnailGeneration',
|
||||
'videoConversion',
|
||||
'workflow',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
14
mobile/openapi/lib/model/system_config_job_dto.dart
generated
14
mobile/openapi/lib/model/system_config_job_dto.dart
generated
@@ -25,6 +25,7 @@ class SystemConfigJobDto {
|
||||
required this.smartSearch,
|
||||
required this.thumbnailGeneration,
|
||||
required this.videoConversion,
|
||||
required this.workflow,
|
||||
});
|
||||
|
||||
JobSettingsDto backgroundTask;
|
||||
@@ -51,6 +52,8 @@ class SystemConfigJobDto {
|
||||
|
||||
JobSettingsDto videoConversion;
|
||||
|
||||
JobSettingsDto workflow;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigJobDto &&
|
||||
other.backgroundTask == backgroundTask &&
|
||||
@@ -64,7 +67,8 @@ class SystemConfigJobDto {
|
||||
other.sidecar == sidecar &&
|
||||
other.smartSearch == smartSearch &&
|
||||
other.thumbnailGeneration == thumbnailGeneration &&
|
||||
other.videoConversion == videoConversion;
|
||||
other.videoConversion == videoConversion &&
|
||||
other.workflow == workflow;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -80,10 +84,11 @@ class SystemConfigJobDto {
|
||||
(sidecar.hashCode) +
|
||||
(smartSearch.hashCode) +
|
||||
(thumbnailGeneration.hashCode) +
|
||||
(videoConversion.hashCode);
|
||||
(videoConversion.hashCode) +
|
||||
(workflow.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -99,6 +104,7 @@ class SystemConfigJobDto {
|
||||
json[r'smartSearch'] = this.smartSearch;
|
||||
json[r'thumbnailGeneration'] = this.thumbnailGeneration;
|
||||
json[r'videoConversion'] = this.videoConversion;
|
||||
json[r'workflow'] = this.workflow;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -123,6 +129,7 @@ class SystemConfigJobDto {
|
||||
smartSearch: JobSettingsDto.fromJson(json[r'smartSearch'])!,
|
||||
thumbnailGeneration: JobSettingsDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: JobSettingsDto.fromJson(json[r'videoConversion'])!,
|
||||
workflow: JobSettingsDto.fromJson(json[r'workflow'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -182,6 +189,7 @@ class SystemConfigJobDto {
|
||||
'smartSearch',
|
||||
'thumbnailGeneration',
|
||||
'videoConversion',
|
||||
'workflow',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
116
mobile/openapi/lib/model/workflow_action_item_dto.dart
generated
Normal file
116
mobile/openapi/lib/model/workflow_action_item_dto.dart
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// 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.actionId,
|
||||
});
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
String actionId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto &&
|
||||
other.actionConfig == actionConfig &&
|
||||
other.actionId == actionId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actionConfig == null ? 0 : actionConfig!.hashCode) +
|
||||
(actionId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowActionItemDto[actionConfig=$actionConfig, actionId=$actionId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.actionConfig != null) {
|
||||
json[r'actionConfig'] = this.actionConfig;
|
||||
} else {
|
||||
// json[r'actionConfig'] = null;
|
||||
}
|
||||
json[r'actionId'] = this.actionId;
|
||||
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'),
|
||||
actionId: mapValueOfType<String>(json, r'actionId')!,
|
||||
);
|
||||
}
|
||||
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>{
|
||||
'actionId',
|
||||
};
|
||||
}
|
||||
|
||||
135
mobile/openapi/lib/model/workflow_action_response_dto.dart
generated
Normal file
135
mobile/openapi/lib/model/workflow_action_response_dto.dart
generated
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// 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.actionId,
|
||||
required this.id,
|
||||
required this.order,
|
||||
required this.workflowId,
|
||||
});
|
||||
|
||||
Object? actionConfig;
|
||||
|
||||
String actionId;
|
||||
|
||||
String id;
|
||||
|
||||
num order;
|
||||
|
||||
String workflowId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto &&
|
||||
other.actionConfig == actionConfig &&
|
||||
other.actionId == actionId &&
|
||||
other.id == id &&
|
||||
other.order == order &&
|
||||
other.workflowId == workflowId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actionConfig == null ? 0 : actionConfig!.hashCode) +
|
||||
(actionId.hashCode) +
|
||||
(id.hashCode) +
|
||||
(order.hashCode) +
|
||||
(workflowId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowActionResponseDto[actionConfig=$actionConfig, actionId=$actionId, id=$id, order=$order, 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'actionId'] = this.actionId;
|
||||
json[r'id'] = this.id;
|
||||
json[r'order'] = this.order;
|
||||
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'),
|
||||
actionId: mapValueOfType<String>(json, r'actionId')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
order: num.parse('${json[r'order']}'),
|
||||
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',
|
||||
'actionId',
|
||||
'id',
|
||||
'order',
|
||||
'workflowId',
|
||||
};
|
||||
}
|
||||
|
||||
157
mobile/openapi/lib/model/workflow_create_dto.dart
generated
Normal file
157
mobile/openapi/lib/model/workflow_create_dto.dart
generated
Normal file
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// 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 WorkflowCreateDto {
|
||||
/// Returns a new [WorkflowCreateDto] instance.
|
||||
WorkflowCreateDto({
|
||||
this.actions = const [],
|
||||
this.description,
|
||||
this.enabled,
|
||||
this.filters = const [],
|
||||
required this.name,
|
||||
required this.triggerType,
|
||||
});
|
||||
|
||||
List<WorkflowActionItemDto> actions;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
List<WorkflowFilterItemDto> filters;
|
||||
|
||||
String name;
|
||||
|
||||
PluginTriggerType triggerType;
|
||||
|
||||
@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;
|
||||
|
||||
@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);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowCreateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
if (this.enabled != null) {
|
||||
json[r'enabled'] = this.enabled;
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
json[r'filters'] = this.filters;
|
||||
json[r'name'] = this.name;
|
||||
json[r'triggerType'] = this.triggerType;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowCreateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowCreateDto");
|
||||
if (value is Map) {
|
||||
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'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowCreateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowCreateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowCreateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowCreateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowCreateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowCreateDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowCreateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowCreateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'actions',
|
||||
'filters',
|
||||
'name',
|
||||
'triggerType',
|
||||
};
|
||||
}
|
||||
|
||||
116
mobile/openapi/lib/model/workflow_filter_item_dto.dart
generated
Normal file
116
mobile/openapi/lib/model/workflow_filter_item_dto.dart
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// 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.filterId,
|
||||
});
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
String filterId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto &&
|
||||
other.filterConfig == filterConfig &&
|
||||
other.filterId == filterId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(filterConfig == null ? 0 : filterConfig!.hashCode) +
|
||||
(filterId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowFilterItemDto[filterConfig=$filterConfig, filterId=$filterId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.filterConfig != null) {
|
||||
json[r'filterConfig'] = this.filterConfig;
|
||||
} else {
|
||||
// json[r'filterConfig'] = null;
|
||||
}
|
||||
json[r'filterId'] = this.filterId;
|
||||
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'),
|
||||
filterId: mapValueOfType<String>(json, r'filterId')!,
|
||||
);
|
||||
}
|
||||
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>{
|
||||
'filterId',
|
||||
};
|
||||
}
|
||||
|
||||
135
mobile/openapi/lib/model/workflow_filter_response_dto.dart
generated
Normal file
135
mobile/openapi/lib/model/workflow_filter_response_dto.dart
generated
Normal file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// 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.filterId,
|
||||
required this.id,
|
||||
required this.order,
|
||||
required this.workflowId,
|
||||
});
|
||||
|
||||
Object? filterConfig;
|
||||
|
||||
String filterId;
|
||||
|
||||
String id;
|
||||
|
||||
num order;
|
||||
|
||||
String workflowId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto &&
|
||||
other.filterConfig == filterConfig &&
|
||||
other.filterId == filterId &&
|
||||
other.id == id &&
|
||||
other.order == order &&
|
||||
other.workflowId == workflowId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(filterConfig == null ? 0 : filterConfig!.hashCode) +
|
||||
(filterId.hashCode) +
|
||||
(id.hashCode) +
|
||||
(order.hashCode) +
|
||||
(workflowId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowFilterResponseDto[filterConfig=$filterConfig, filterId=$filterId, id=$id, order=$order, 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'filterId'] = this.filterId;
|
||||
json[r'id'] = this.id;
|
||||
json[r'order'] = this.order;
|
||||
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'),
|
||||
filterId: mapValueOfType<String>(json, r'filterId')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
order: num.parse('${json[r'order']}'),
|
||||
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',
|
||||
'filterId',
|
||||
'id',
|
||||
'order',
|
||||
'workflowId',
|
||||
};
|
||||
}
|
||||
|
||||
241
mobile/openapi/lib/model/workflow_response_dto.dart
generated
Normal file
241
mobile/openapi/lib/model/workflow_response_dto.dart
generated
Normal file
@@ -0,0 +1,241 @@
|
||||
//
|
||||
// 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 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,
|
||||
});
|
||||
|
||||
List<WorkflowActionResponseDto> actions;
|
||||
|
||||
String createdAt;
|
||||
|
||||
String description;
|
||||
|
||||
bool enabled;
|
||||
|
||||
List<WorkflowFilterResponseDto> filters;
|
||||
|
||||
String id;
|
||||
|
||||
String? name;
|
||||
|
||||
String ownerId;
|
||||
|
||||
WorkflowResponseDtoTriggerTypeEnum triggerType;
|
||||
|
||||
@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;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(actions.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(description.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(filters.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(triggerType.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowResponseDto[actions=$actions, createdAt=$createdAt, description=$description, enabled=$enabled, filters=$filters, id=$id, name=$name, ownerId=$ownerId, triggerType=$triggerType]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'description'] = this.description;
|
||||
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;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowResponseDto");
|
||||
if (value is Map) {
|
||||
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')!,
|
||||
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: WorkflowResponseDtoTriggerTypeEnum.fromJson(json[r'triggerType'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowResponseDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// 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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class WorkflowResponseDtoTriggerTypeEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const WorkflowResponseDtoTriggerTypeEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const assetCreate = WorkflowResponseDtoTriggerTypeEnum._(r'AssetCreate');
|
||||
static const personRecognized = WorkflowResponseDtoTriggerTypeEnum._(r'PersonRecognized');
|
||||
|
||||
/// List of all possible values in this [enum][WorkflowResponseDtoTriggerTypeEnum].
|
||||
static const values = <WorkflowResponseDtoTriggerTypeEnum>[
|
||||
assetCreate,
|
||||
personRecognized,
|
||||
];
|
||||
|
||||
static WorkflowResponseDtoTriggerTypeEnum? fromJson(dynamic value) => WorkflowResponseDtoTriggerTypeEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<WorkflowResponseDtoTriggerTypeEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowResponseDtoTriggerTypeEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowResponseDtoTriggerTypeEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [WorkflowResponseDtoTriggerTypeEnum] to String,
|
||||
/// and [decode] dynamic data back to [WorkflowResponseDtoTriggerTypeEnum].
|
||||
class WorkflowResponseDtoTriggerTypeEnumTypeTransformer {
|
||||
factory WorkflowResponseDtoTriggerTypeEnumTypeTransformer() => _instance ??= const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._();
|
||||
|
||||
const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._();
|
||||
|
||||
String encode(WorkflowResponseDtoTriggerTypeEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a WorkflowResponseDtoTriggerTypeEnum.
|
||||
///
|
||||
/// 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.
|
||||
WorkflowResponseDtoTriggerTypeEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'AssetCreate': return WorkflowResponseDtoTriggerTypeEnum.assetCreate;
|
||||
case r'PersonRecognized': return WorkflowResponseDtoTriggerTypeEnum.personRecognized;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [WorkflowResponseDtoTriggerTypeEnumTypeTransformer] instance.
|
||||
static WorkflowResponseDtoTriggerTypeEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
156
mobile/openapi/lib/model/workflow_update_dto.dart
generated
Normal file
156
mobile/openapi/lib/model/workflow_update_dto.dart
generated
Normal file
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// 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 WorkflowUpdateDto {
|
||||
/// Returns a new [WorkflowUpdateDto] instance.
|
||||
WorkflowUpdateDto({
|
||||
this.actions = const [],
|
||||
this.description,
|
||||
this.enabled,
|
||||
this.filters = const [],
|
||||
this.name,
|
||||
});
|
||||
|
||||
List<WorkflowActionItemDto> actions;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
List<WorkflowFilterItemDto> filters;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
@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;
|
||||
|
||||
@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);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'actions'] = this.actions;
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
if (this.enabled != null) {
|
||||
json[r'enabled'] = this.enabled;
|
||||
} else {
|
||||
// json[r'enabled'] = null;
|
||||
}
|
||||
json[r'filters'] = this.filters;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowUpdateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowUpdateDto");
|
||||
if (value is Map) {
|
||||
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'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7865,6 +7865,111 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/plugins": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of plugins available to the authenticated user.",
|
||||
"operationId": "getPlugins",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PluginResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "List all plugins",
|
||||
"tags": [
|
||||
"Plugins"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "plugin.read",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/plugins/{id}": {
|
||||
"get": {
|
||||
"description": "Retrieve information about a specific plugin by its ID.",
|
||||
"operationId": "getPlugin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PluginResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Retrieve a plugin",
|
||||
"tags": [
|
||||
"Plugins"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "plugin.read",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/search/cities": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.",
|
||||
@@ -13485,6 +13590,276 @@
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/workflows": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of workflows available to the authenticated user.",
|
||||
"operationId": "getWorkflows",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "List all workflows",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.read",
|
||||
"x-immich-state": "Alpha"
|
||||
},
|
||||
"post": {
|
||||
"description": "Create a new workflow, the workflow can also be created with empty filters and actions.",
|
||||
"operationId": "createWorkflow",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowCreateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Create a workflow",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.create",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/workflows/{id}": {
|
||||
"delete": {
|
||||
"description": "Delete a workflow by its ID.",
|
||||
"operationId": "deleteWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete a workflow",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.delete",
|
||||
"x-immich-state": "Alpha"
|
||||
},
|
||||
"get": {
|
||||
"description": "Retrieve information about a specific workflow by its ID.",
|
||||
"operationId": "getWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Retrieve a workflow",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.read",
|
||||
"x-immich-state": "Alpha"
|
||||
},
|
||||
"put": {
|
||||
"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.",
|
||||
"operationId": "updateWorkflow",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Update a workflow",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.3.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.update",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@@ -13566,6 +13941,10 @@
|
||||
"name": "People",
|
||||
"description": "A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job."
|
||||
},
|
||||
{
|
||||
"name": "Plugins",
|
||||
"description": "A plugin is an installed module that makes filters and actions available for the workflow feature."
|
||||
},
|
||||
{
|
||||
"name": "Search",
|
||||
"description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting."
|
||||
@@ -13621,6 +14000,10 @@
|
||||
{
|
||||
"name": "Views",
|
||||
"description": "Endpoints for specialized views, such as the folder view."
|
||||
},
|
||||
{
|
||||
"name": "Workflows",
|
||||
"description": "A workflow is a set of actions that run whenever a triggering event occurs. Workflows also can include filters to further limit execution."
|
||||
}
|
||||
],
|
||||
"servers": [
|
||||
@@ -17022,6 +17405,10 @@
|
||||
"pinCode.create",
|
||||
"pinCode.update",
|
||||
"pinCode.delete",
|
||||
"plugin.create",
|
||||
"plugin.read",
|
||||
"plugin.update",
|
||||
"plugin.delete",
|
||||
"server.about",
|
||||
"server.apkLinks",
|
||||
"server.storage",
|
||||
@@ -17071,6 +17458,10 @@
|
||||
"userProfileImage.read",
|
||||
"userProfileImage.update",
|
||||
"userProfileImage.delete",
|
||||
"workflow.create",
|
||||
"workflow.read",
|
||||
"workflow.update",
|
||||
"workflow.delete",
|
||||
"adminUser.create",
|
||||
"adminUser.read",
|
||||
"adminUser.update",
|
||||
@@ -17367,6 +17758,152 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginActionResponseDto": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"methodName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"schema": {
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"supportedContexts": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PluginContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"methodName",
|
||||
"pluginId",
|
||||
"schema",
|
||||
"supportedContexts",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginContext": {
|
||||
"enum": [
|
||||
"asset",
|
||||
"album",
|
||||
"person"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginFilterResponseDto": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"methodName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": "string"
|
||||
},
|
||||
"schema": {
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"supportedContexts": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PluginContext"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"methodName",
|
||||
"pluginId",
|
||||
"schema",
|
||||
"supportedContexts",
|
||||
"title"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginResponseDto": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PluginActionResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"filters": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PluginFilterResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actions",
|
||||
"author",
|
||||
"createdAt",
|
||||
"description",
|
||||
"filters",
|
||||
"id",
|
||||
"name",
|
||||
"title",
|
||||
"updatedAt",
|
||||
"version"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginTriggerType": {
|
||||
"enum": [
|
||||
"AssetCreate",
|
||||
"PersonRecognized"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PurchaseResponse": {
|
||||
"properties": {
|
||||
"hideBuyButtonUntil": {
|
||||
@@ -17438,7 +17975,8 @@
|
||||
"library",
|
||||
"notifications",
|
||||
"backupDatabase",
|
||||
"ocr"
|
||||
"ocr",
|
||||
"workflow"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17552,6 +18090,9 @@
|
||||
},
|
||||
"videoConversion": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"workflow": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -17570,7 +18111,8 @@
|
||||
"smartSearch",
|
||||
"storageTemplateMigration",
|
||||
"thumbnailGeneration",
|
||||
"videoConversion"
|
||||
"videoConversion",
|
||||
"workflow"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -20420,6 +20962,9 @@
|
||||
},
|
||||
"videoConversion": {
|
||||
"$ref": "#/components/schemas/JobSettingsDto"
|
||||
},
|
||||
"workflow": {
|
||||
"$ref": "#/components/schemas/JobSettingsDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -20434,7 +20979,8 @@
|
||||
"sidecar",
|
||||
"smartSearch",
|
||||
"thumbnailGeneration",
|
||||
"videoConversion"
|
||||
"videoConversion",
|
||||
"workflow"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -21999,6 +22545,211 @@
|
||||
"webm"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkflowActionItemDto": {
|
||||
"properties": {
|
||||
"actionConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"actionId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actionId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowActionResponseDto": {
|
||||
"properties": {
|
||||
"actionConfig": {
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"actionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "number"
|
||||
},
|
||||
"workflowId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actionConfig",
|
||||
"actionId",
|
||||
"id",
|
||||
"order",
|
||||
"workflowId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowCreateDto": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowActionItemDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"filters": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowFilterItemDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"triggerType": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PluginTriggerType"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actions",
|
||||
"filters",
|
||||
"name",
|
||||
"triggerType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowFilterItemDto": {
|
||||
"properties": {
|
||||
"filterConfig": {
|
||||
"type": "object"
|
||||
},
|
||||
"filterId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"filterId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowFilterResponseDto": {
|
||||
"properties": {
|
||||
"filterConfig": {
|
||||
"nullable": true,
|
||||
"type": "object"
|
||||
},
|
||||
"filterId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "number"
|
||||
},
|
||||
"workflowId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"filterConfig",
|
||||
"filterId",
|
||||
"id",
|
||||
"order",
|
||||
"workflowId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowResponseDto": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowActionResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"filters": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowFilterResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"triggerType": {
|
||||
"enum": [
|
||||
"AssetCreate",
|
||||
"PersonRecognized"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actions",
|
||||
"createdAt",
|
||||
"description",
|
||||
"enabled",
|
||||
"filters",
|
||||
"id",
|
||||
"name",
|
||||
"ownerId",
|
||||
"triggerType"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowUpdateDto": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowActionItemDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"filters": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowFilterItemDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -732,6 +732,7 @@ export type QueuesResponseDto = {
|
||||
storageTemplateMigration: QueueResponseDto;
|
||||
thumbnailGeneration: QueueResponseDto;
|
||||
videoConversion: QueueResponseDto;
|
||||
workflow: QueueResponseDto;
|
||||
};
|
||||
export type JobCreateDto = {
|
||||
name: ManualJobName;
|
||||
@@ -926,6 +927,36 @@ export type AssetFaceUpdateDto = {
|
||||
export type PersonStatisticsResponseDto = {
|
||||
assets: number;
|
||||
};
|
||||
export type PluginActionResponseDto = {
|
||||
description: string;
|
||||
id: string;
|
||||
methodName: string;
|
||||
pluginId: string;
|
||||
schema: object | null;
|
||||
supportedContexts: PluginContext[];
|
||||
title: string;
|
||||
};
|
||||
export type PluginFilterResponseDto = {
|
||||
description: string;
|
||||
id: string;
|
||||
methodName: string;
|
||||
pluginId: string;
|
||||
schema: object | null;
|
||||
supportedContexts: PluginContext[];
|
||||
title: string;
|
||||
};
|
||||
export type PluginResponseDto = {
|
||||
actions: PluginActionResponseDto[];
|
||||
author: string;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
filters: PluginFilterResponseDto[];
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
version: string;
|
||||
};
|
||||
export type SearchExploreItem = {
|
||||
data: AssetResponseDto;
|
||||
value: string;
|
||||
@@ -1411,6 +1442,7 @@ export type SystemConfigJobDto = {
|
||||
smartSearch: JobSettingsDto;
|
||||
thumbnailGeneration: JobSettingsDto;
|
||||
videoConversion: JobSettingsDto;
|
||||
workflow: JobSettingsDto;
|
||||
};
|
||||
export type SystemConfigLibraryScanDto = {
|
||||
cronExpression: string;
|
||||
@@ -1667,6 +1699,54 @@ export type CreateProfileImageResponseDto = {
|
||||
profileImagePath: string;
|
||||
userId: string;
|
||||
};
|
||||
export type WorkflowActionResponseDto = {
|
||||
actionConfig: object | null;
|
||||
actionId: string;
|
||||
id: string;
|
||||
order: number;
|
||||
workflowId: string;
|
||||
};
|
||||
export type WorkflowFilterResponseDto = {
|
||||
filterConfig: object | null;
|
||||
filterId: string;
|
||||
id: string;
|
||||
order: number;
|
||||
workflowId: string;
|
||||
};
|
||||
export type WorkflowResponseDto = {
|
||||
actions: WorkflowActionResponseDto[];
|
||||
createdAt: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
filters: WorkflowFilterResponseDto[];
|
||||
id: string;
|
||||
name: string | null;
|
||||
ownerId: string;
|
||||
triggerType: TriggerType;
|
||||
};
|
||||
export type WorkflowActionItemDto = {
|
||||
actionConfig?: object;
|
||||
actionId: string;
|
||||
};
|
||||
export type WorkflowFilterItemDto = {
|
||||
filterConfig?: object;
|
||||
filterId: string;
|
||||
};
|
||||
export type WorkflowCreateDto = {
|
||||
actions: WorkflowActionItemDto[];
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
filters: WorkflowFilterItemDto[];
|
||||
name: string;
|
||||
triggerType: PluginTriggerType;
|
||||
};
|
||||
export type WorkflowUpdateDto = {
|
||||
actions?: WorkflowActionItemDto[];
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
filters?: WorkflowFilterItemDto[];
|
||||
name?: string;
|
||||
};
|
||||
/**
|
||||
* List all activities
|
||||
*/
|
||||
@@ -3510,6 +3590,30 @@ export function getPersonThumbnail({ id }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* List all plugins
|
||||
*/
|
||||
export function getPlugins(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PluginResponseDto[];
|
||||
}>("/plugins", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve a plugin
|
||||
*/
|
||||
export function getPlugin({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PluginResponseDto;
|
||||
}>(`/plugins/${encodeURIComponent(id)}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve assets by city
|
||||
*/
|
||||
@@ -4824,6 +4928,72 @@ export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* List all workflows
|
||||
*/
|
||||
export function getWorkflows(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowResponseDto[];
|
||||
}>("/workflows", {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Create a workflow
|
||||
*/
|
||||
export function createWorkflow({ workflowCreateDto }: {
|
||||
workflowCreateDto: WorkflowCreateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: WorkflowResponseDto;
|
||||
}>("/workflows", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: workflowCreateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete a workflow
|
||||
*/
|
||||
export function deleteWorkflow({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/workflows/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve a workflow
|
||||
*/
|
||||
export function getWorkflow({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowResponseDto;
|
||||
}>(`/workflows/${encodeURIComponent(id)}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Update a workflow
|
||||
*/
|
||||
export function updateWorkflow({ id, workflowUpdateDto }: {
|
||||
id: string;
|
||||
workflowUpdateDto: WorkflowUpdateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowResponseDto;
|
||||
}>(`/workflows/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: workflowUpdateDto
|
||||
})));
|
||||
}
|
||||
export enum ReactionLevel {
|
||||
Album = "album",
|
||||
Asset = "asset"
|
||||
@@ -4976,6 +5146,10 @@ export enum Permission {
|
||||
PinCodeCreate = "pinCode.create",
|
||||
PinCodeUpdate = "pinCode.update",
|
||||
PinCodeDelete = "pinCode.delete",
|
||||
PluginCreate = "plugin.create",
|
||||
PluginRead = "plugin.read",
|
||||
PluginUpdate = "plugin.update",
|
||||
PluginDelete = "plugin.delete",
|
||||
ServerAbout = "server.about",
|
||||
ServerApkLinks = "server.apkLinks",
|
||||
ServerStorage = "server.storage",
|
||||
@@ -5025,6 +5199,10 @@ export enum Permission {
|
||||
UserProfileImageRead = "userProfileImage.read",
|
||||
UserProfileImageUpdate = "userProfileImage.update",
|
||||
UserProfileImageDelete = "userProfileImage.delete",
|
||||
WorkflowCreate = "workflow.create",
|
||||
WorkflowRead = "workflow.read",
|
||||
WorkflowUpdate = "workflow.update",
|
||||
WorkflowDelete = "workflow.delete",
|
||||
AdminUserCreate = "adminUser.create",
|
||||
AdminUserRead = "adminUser.read",
|
||||
AdminUserUpdate = "adminUser.update",
|
||||
@@ -5083,7 +5261,8 @@ export enum QueueName {
|
||||
Library = "library",
|
||||
Notifications = "notifications",
|
||||
BackupDatabase = "backupDatabase",
|
||||
Ocr = "ocr"
|
||||
Ocr = "ocr",
|
||||
Workflow = "workflow"
|
||||
}
|
||||
export enum QueueCommand {
|
||||
Start = "start",
|
||||
@@ -5104,6 +5283,11 @@ export enum PartnerDirection {
|
||||
SharedBy = "shared-by",
|
||||
SharedWith = "shared-with"
|
||||
}
|
||||
export enum PluginContext {
|
||||
Asset = "asset",
|
||||
Album = "album",
|
||||
Person = "person"
|
||||
}
|
||||
export enum SearchSuggestionType {
|
||||
Country = "country",
|
||||
State = "state",
|
||||
@@ -5255,3 +5439,11 @@ export enum OAuthTokenEndpointAuthMethod {
|
||||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
}
|
||||
export enum TriggerType {
|
||||
AssetCreate = "AssetCreate",
|
||||
PersonRecognized = "PersonRecognized"
|
||||
}
|
||||
export enum PluginTriggerType {
|
||||
AssetCreate = "AssetCreate",
|
||||
PersonRecognized = "PersonRecognized"
|
||||
}
|
||||
|
||||
2
plugins/.gitignore
vendored
Normal file
2
plugins/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
26
plugins/LICENSE
Normal file
26
plugins/LICENSE
Normal file
@@ -0,0 +1,26 @@
|
||||
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.
|
||||
12
plugins/esbuild.js
Normal file
12
plugins/esbuild.js
Normal file
@@ -0,0 +1,12 @@
|
||||
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
|
||||
})
|
||||
127
plugins/manifest.json
Normal file
127
plugins/manifest.json
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "immich-core",
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"description": "Text or regex pattern to match against filename"
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"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",
|
||||
"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",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of person to match"
|
||||
},
|
||||
"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",
|
||||
"description": "Target album ID"
|
||||
}
|
||||
},
|
||||
"required": ["albumId"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
plugins/mise.toml
Normal file
11
plugins/mise.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[tools]
|
||||
"github:extism/cli" = "1.6.3"
|
||||
"github:webassembly/binaryen" = "version_124"
|
||||
"github:extism/js-pdk" = "1.5.1"
|
||||
|
||||
[tasks.install]
|
||||
run = "pnpm install --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
depends = ["install"]
|
||||
run = "pnpm run build"
|
||||
443
plugins/package-lock.json
generated
Normal file
443
plugins/package-lock.json
generated
Normal file
@@ -0,0 +1,443 @@
|
||||
{
|
||||
"name": "js-pdk-template",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "js-pdk-template",
|
||||
"version": "1.0.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.19.6",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
|
||||
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
|
||||
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
|
||||
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
|
||||
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
|
||||
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
|
||||
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
|
||||
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
|
||||
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
|
||||
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@extism/js-pdk": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
|
||||
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
|
||||
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.19.12",
|
||||
"@esbuild/android-arm": "0.19.12",
|
||||
"@esbuild/android-arm64": "0.19.12",
|
||||
"@esbuild/android-x64": "0.19.12",
|
||||
"@esbuild/darwin-arm64": "0.19.12",
|
||||
"@esbuild/darwin-x64": "0.19.12",
|
||||
"@esbuild/freebsd-arm64": "0.19.12",
|
||||
"@esbuild/freebsd-x64": "0.19.12",
|
||||
"@esbuild/linux-arm": "0.19.12",
|
||||
"@esbuild/linux-arm64": "0.19.12",
|
||||
"@esbuild/linux-ia32": "0.19.12",
|
||||
"@esbuild/linux-loong64": "0.19.12",
|
||||
"@esbuild/linux-mips64el": "0.19.12",
|
||||
"@esbuild/linux-ppc64": "0.19.12",
|
||||
"@esbuild/linux-riscv64": "0.19.12",
|
||||
"@esbuild/linux-s390x": "0.19.12",
|
||||
"@esbuild/linux-x64": "0.19.12",
|
||||
"@esbuild/netbsd-x64": "0.19.12",
|
||||
"@esbuild/openbsd-x64": "0.19.12",
|
||||
"@esbuild/sunos-x64": "0.19.12",
|
||||
"@esbuild/win32-arm64": "0.19.12",
|
||||
"@esbuild/win32-ia32": "0.19.12",
|
||||
"@esbuild/win32-x64": "0.19.12"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
plugins/package.json
Normal file
19
plugins/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm build:tsc && pnpm build:wasm",
|
||||
"build:tsc": "tsc --noEmit && node esbuild.js",
|
||||
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.0.1",
|
||||
"esbuild": "^0.19.6",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
12
plugins/src/index.d.ts
vendored
Normal file
12
plugins/src/index.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
71
plugins/src/index.ts
Normal file
71
plugins/src/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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 });
|
||||
}
|
||||
24
plugins/tsconfig.json
Normal file
24
plugins/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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
|
||||
"strict": true, // Enable all strict type-checking options
|
||||
"esModuleInterop": true, // Enables compatibility with Babel-style module imports
|
||||
"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)
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts" // Include all TypeScript files in src directory
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules" // Exclude the node_modules directory
|
||||
]
|
||||
}
|
||||
130
pnpm-lock.yaml
generated
130
pnpm-lock.yaml
generated
@@ -299,8 +299,23 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.9.3
|
||||
|
||||
plugins:
|
||||
devDependencies:
|
||||
'@extism/js-pdk':
|
||||
specifier: ^1.0.1
|
||||
version: 1.1.1
|
||||
esbuild:
|
||||
specifier: ^0.19.6
|
||||
version: 0.19.12
|
||||
typescript:
|
||||
specifier: ^5.3.2
|
||||
version: 5.9.3
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
'@extism/extism':
|
||||
specifier: 2.0.0-rc13
|
||||
version: 2.0.0-rc13
|
||||
'@nestjs/bullmq':
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.4(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(bullmq@5.62.1)
|
||||
@@ -367,6 +382,9 @@ importers:
|
||||
'@socket.io/redis-adapter':
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(socket.io-adapter@2.5.5)
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.17.1
|
||||
archiver:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1
|
||||
@@ -430,6 +448,9 @@ importers:
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
kysely:
|
||||
specifier: 0.28.2
|
||||
version: 0.28.2
|
||||
@@ -569,6 +590,9 @@ importers:
|
||||
'@types/js-yaml':
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.10
|
||||
version: 9.0.10
|
||||
'@types/lodash':
|
||||
specifier: ^4.14.197
|
||||
version: 4.17.20
|
||||
@@ -2572,6 +2596,12 @@ packages:
|
||||
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@extism/extism@2.0.0-rc13':
|
||||
resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==}
|
||||
|
||||
'@extism/js-pdk@1.1.1':
|
||||
resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==}
|
||||
|
||||
'@faker-js/faker@10.1.0':
|
||||
resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
|
||||
@@ -4590,6 +4620,9 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||
|
||||
'@types/justified-layout@4.1.4':
|
||||
resolution: {integrity: sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==}
|
||||
|
||||
@@ -5364,6 +5397,9 @@ packages:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -6294,6 +6330,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
@@ -7717,12 +7756,22 @@ packages:
|
||||
jsonfile@6.2.0:
|
||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
just-compare@2.3.0:
|
||||
resolution: {integrity: sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==}
|
||||
|
||||
justified-layout@4.1.0:
|
||||
resolution: {integrity: sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==}
|
||||
|
||||
jwa@1.4.2:
|
||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
|
||||
kdbush@3.0.0:
|
||||
resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==}
|
||||
|
||||
@@ -7922,15 +7971,36 @@ packages:
|
||||
lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
|
||||
lodash.includes@4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
|
||||
lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
|
||||
lodash.isboolean@3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
|
||||
lodash.isinteger@4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
|
||||
lodash.isnumber@3.0.3:
|
||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
lodash.isstring@4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
|
||||
lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
lodash.uniq@4.5.0:
|
||||
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
|
||||
|
||||
@@ -11043,6 +11113,9 @@ packages:
|
||||
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
urlpattern-polyfill@8.0.2:
|
||||
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
@@ -14170,6 +14243,12 @@ snapshots:
|
||||
'@eslint/core': 0.16.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@extism/extism@2.0.0-rc13': {}
|
||||
|
||||
'@extism/js-pdk@1.1.1':
|
||||
dependencies:
|
||||
urlpattern-polyfill: 8.0.2
|
||||
|
||||
'@faker-js/faker@10.1.0': {}
|
||||
|
||||
'@fig/complete-commander@3.2.0(commander@11.1.0)':
|
||||
@@ -16414,6 +16493,11 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/jsonwebtoken@9.0.10':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
'@types/node': 22.19.0
|
||||
|
||||
'@types/justified-layout@4.1.4': {}
|
||||
|
||||
'@types/keygrip@1.0.6': {}
|
||||
@@ -17389,6 +17473,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -18341,6 +18427,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.243: {}
|
||||
@@ -20155,10 +20245,34 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
lodash.includes: 4.3.0
|
||||
lodash.isboolean: 3.0.3
|
||||
lodash.isinteger: 4.0.4
|
||||
lodash.isnumber: 3.0.3
|
||||
lodash.isplainobject: 4.0.6
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.7.3
|
||||
|
||||
just-compare@2.3.0: {}
|
||||
|
||||
justified-layout@4.1.0: {}
|
||||
|
||||
jwa@1.4.2:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@3.2.2:
|
||||
dependencies:
|
||||
jwa: 1.4.2
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
kdbush@3.0.0: {}
|
||||
|
||||
kdbush@4.0.2: {}
|
||||
@@ -20323,12 +20437,26 @@ snapshots:
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
|
||||
lodash.includes@4.3.0: {}
|
||||
|
||||
lodash.isarguments@3.1.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
|
||||
lodash.isinteger@4.0.4: {}
|
||||
|
||||
lodash.isnumber@3.0.3: {}
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.isstring@4.0.1: {}
|
||||
|
||||
lodash.memoize@4.1.2: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash.uniq@4.5.0: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
@@ -24145,6 +24273,8 @@ snapshots:
|
||||
punycode: 1.4.1
|
||||
qs: 6.14.0
|
||||
|
||||
urlpattern-polyfill@8.0.2: {}
|
||||
|
||||
use-sync-external-store@1.6.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
@@ -4,6 +4,7 @@ packages:
|
||||
- e2e
|
||||
- open-api/typescript-sdk
|
||||
- server
|
||||
- plugins
|
||||
- web
|
||||
- .github
|
||||
ignoredBuiltDependencies:
|
||||
|
||||
@@ -48,6 +48,24 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
||||
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||
|
||||
FROM builder AS plugins
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2025.11.3 /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
|
||||
RUN mise install --cd plugins
|
||||
|
||||
COPY ./plugins ./plugins/
|
||||
# Build plugins
|
||||
RUN --mount=type=cache,id=pnpm-plugins,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 \
|
||||
cd plugins && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511041104@sha256:57c0379977fd5521d83cdf661aecd1497c83a9a661ebafe0a5243a09fc1064cb
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
@@ -58,6 +76,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
|
||||
RUN ln -s ../../cli/bin/immich server/bin/immich
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"email:dev": "email dev -p 3050 --dir src/emails"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extism/extism": "2.0.0-rc13",
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
"@nestjs/core": "^11.0.4",
|
||||
@@ -56,6 +57,7 @@
|
||||
"@react-email/components": "^0.5.0",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"ajv": "^8.17.1",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
@@ -77,6 +79,7 @@
|
||||
"i18n-iso-countries": "^7.6.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "0.28.2",
|
||||
"kysely-postgres-js": "^3.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -124,6 +127,7 @@
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/fluent-ffmpeg": "^2.1.21",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/luxon": "^3.6.2",
|
||||
|
||||
@@ -235,6 +235,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
[QueueName.VideoConversion]: { concurrency: 1 },
|
||||
[QueueName.Notification]: { concurrency: 5 },
|
||||
[QueueName.Ocr]: { concurrency: 1 },
|
||||
[QueueName.Workflow]: { concurrency: 5 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
|
||||
@@ -160,6 +160,8 @@ export const endpointTags: Record<ApiTag, string> = {
|
||||
[ApiTag.Partners]: 'A partner is a link with another user that allows sharing of assets between two users.',
|
||||
[ApiTag.People]:
|
||||
'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.',
|
||||
[ApiTag.Plugins]:
|
||||
'A plugin is an installed module that makes filters and actions available for the workflow feature.',
|
||||
[ApiTag.Search]:
|
||||
'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.',
|
||||
[ApiTag.Server]:
|
||||
@@ -185,4 +187,6 @@ export const endpointTags: Record<ApiTag, string> = {
|
||||
[ApiTag.Users]:
|
||||
'Endpoints for viewing and updating the current users, including product key information, profile picture data, onboarding progress, and more.',
|
||||
[ApiTag.Views]: 'Endpoints for specialized views, such as the folder view.',
|
||||
[ApiTag.Workflows]:
|
||||
'A workflow is a set of actions that run whenever a triggering event occurs. Workflows also can include filters to further limit execution.',
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import { NotificationController } from 'src/controllers/notification.controller'
|
||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||
import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { PersonController } from 'src/controllers/person.controller';
|
||||
import { PluginController } from 'src/controllers/plugin.controller';
|
||||
import { SearchController } from 'src/controllers/search.controller';
|
||||
import { ServerController } from 'src/controllers/server.controller';
|
||||
import { SessionController } from 'src/controllers/session.controller';
|
||||
@@ -32,6 +33,7 @@ import { TrashController } from 'src/controllers/trash.controller';
|
||||
import { UserAdminController } from 'src/controllers/user-admin.controller';
|
||||
import { UserController } from 'src/controllers/user.controller';
|
||||
import { ViewController } from 'src/controllers/view.controller';
|
||||
import { WorkflowController } from 'src/controllers/workflow.controller';
|
||||
|
||||
export const controllers = [
|
||||
ApiKeyController,
|
||||
@@ -54,6 +56,7 @@ export const controllers = [
|
||||
OAuthController,
|
||||
PartnerController,
|
||||
PersonController,
|
||||
PluginController,
|
||||
SearchController,
|
||||
ServerController,
|
||||
SessionController,
|
||||
@@ -68,4 +71,5 @@ export const controllers = [
|
||||
UserAdminController,
|
||||
UserController,
|
||||
ViewController,
|
||||
WorkflowController,
|
||||
];
|
||||
|
||||
36
server/src/controllers/plugin.controller.ts
Normal file
36
server/src/controllers/plugin.controller.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { PluginResponseDto } from 'src/dtos/plugin.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PluginService } from 'src/services/plugin.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Plugins')
|
||||
@Controller('plugins')
|
||||
export class PluginController {
|
||||
constructor(private service: PluginService) {}
|
||||
|
||||
@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'),
|
||||
})
|
||||
getPlugins(): Promise<PluginResponseDto[]> {
|
||||
return this.service.getAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.PluginRead })
|
||||
@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'),
|
||||
})
|
||||
getPlugin(@Param() { id }: UUIDParamDto): Promise<PluginResponseDto> {
|
||||
return this.service.get(id);
|
||||
}
|
||||
}
|
||||
76
server/src/controllers/workflow.controller.ts
Normal file
76
server/src/controllers/workflow.controller.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } 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 { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { WorkflowService } from 'src/services/workflow.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Workflows')
|
||||
@Controller('workflows')
|
||||
export class WorkflowController {
|
||||
constructor(private service: WorkflowService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.WorkflowCreate })
|
||||
@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'),
|
||||
})
|
||||
createWorkflow(@Auth() auth: AuthDto, @Body() dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.WorkflowRead })
|
||||
@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'),
|
||||
})
|
||||
getWorkflows(@Auth() auth: AuthDto): Promise<WorkflowResponseDto[]> {
|
||||
return this.service.getAll(auth);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Authenticated({ permission: Permission.WorkflowRead })
|
||||
@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'),
|
||||
})
|
||||
getWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<WorkflowResponseDto> {
|
||||
return this.service.get(auth, id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Authenticated({ permission: Permission.WorkflowUpdate })
|
||||
@Endpoint({
|
||||
summary: 'Update a workflow',
|
||||
description:
|
||||
'Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.',
|
||||
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
|
||||
})
|
||||
updateWorkflow(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: WorkflowUpdateDto,
|
||||
): Promise<WorkflowResponseDto> {
|
||||
return this.service.update(auth, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Authenticated({ permission: Permission.WorkflowDelete })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Delete a workflow',
|
||||
description: 'Delete a workflow by its ID.',
|
||||
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
|
||||
})
|
||||
deleteWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
AssetVisibility,
|
||||
MemoryType,
|
||||
Permission,
|
||||
PluginContext,
|
||||
PluginTriggerType,
|
||||
SharedLinkType,
|
||||
SourceType,
|
||||
UserAvatarColor,
|
||||
@@ -14,7 +16,10 @@ 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 { UserMetadataItem } from 'src/types';
|
||||
import type { ActionConfig, FilterConfig, JSONSchema } from 'src/types/plugin-schema.types';
|
||||
|
||||
export type AuthUser = {
|
||||
id: string;
|
||||
@@ -277,6 +282,45 @@ export type AssetFace = {
|
||||
updateId: string;
|
||||
};
|
||||
|
||||
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;
|
||||
filterId: string;
|
||||
filterConfig: FilterConfig | null;
|
||||
order: number;
|
||||
};
|
||||
|
||||
export type WorkflowAction = Selectable<WorkflowActionTable> & {
|
||||
workflowId: string;
|
||||
actionId: string;
|
||||
actionConfig: ActionConfig | null;
|
||||
order: number;
|
||||
};
|
||||
|
||||
const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
|
||||
const userWithPrefixColumns = [
|
||||
'user2.id',
|
||||
@@ -418,4 +462,15 @@ export const columns = {
|
||||
'asset_exif.state',
|
||||
'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;
|
||||
|
||||
@@ -57,6 +57,13 @@ export class EnvDto {
|
||||
@Type(() => Number)
|
||||
IMMICH_MICROSERVICES_METRICS_PORT?: number;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
IMMICH_PLUGINS_ENABLED?: boolean;
|
||||
|
||||
@Optional()
|
||||
@Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' })
|
||||
IMMICH_PLUGINS_INSTALL_FOLDER?: string;
|
||||
|
||||
@IsInt()
|
||||
@Optional()
|
||||
@Type(() => Number)
|
||||
|
||||
110
server/src/dtos/plugin-manifest.dto.ts
Normal file
110
server/src/dtos/plugin-manifest.dto.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsSemVer,
|
||||
IsString,
|
||||
Matches,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { PluginContext } from 'src/enum';
|
||||
import { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
class PluginManifestWasmDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
path!: string;
|
||||
}
|
||||
|
||||
class PluginManifestFilterDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
methodName!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@IsEnum(PluginContext, { each: true })
|
||||
supportedContexts!: PluginContext[];
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
schema?: JSONSchema;
|
||||
}
|
||||
|
||||
class PluginManifestActionDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
methodName!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true })
|
||||
supportedContexts!: PluginContext[];
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
schema?: JSONSchema;
|
||||
}
|
||||
|
||||
export class PluginManifestDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Matches(/^[a-z0-9-]+[a-z0-9]$/, {
|
||||
message: 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen',
|
||||
})
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsSemVer()
|
||||
version!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
author!: string;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => PluginManifestWasmDto)
|
||||
wasm!: PluginManifestWasmDto;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PluginManifestFilterDto)
|
||||
@IsOptional()
|
||||
filters?: PluginManifestFilterDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PluginManifestActionDto)
|
||||
@IsOptional()
|
||||
actions?: PluginManifestActionDto[];
|
||||
}
|
||||
77
server/src/dtos/plugin.dto.ts
Normal file
77
server/src/dtos/plugin.dto.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { PluginAction, PluginFilter } from 'src/database';
|
||||
import { PluginContext } from 'src/enum';
|
||||
import type { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
export class PluginResponseDto {
|
||||
id!: string;
|
||||
name!: string;
|
||||
title!: string;
|
||||
description!: string;
|
||||
author!: string;
|
||||
version!: string;
|
||||
createdAt!: string;
|
||||
updatedAt!: string;
|
||||
filters!: PluginFilterResponseDto[];
|
||||
actions!: PluginActionResponseDto[];
|
||||
}
|
||||
|
||||
export class PluginFilterResponseDto {
|
||||
id!: string;
|
||||
pluginId!: string;
|
||||
methodName!: string;
|
||||
title!: string;
|
||||
description!: string;
|
||||
|
||||
@ValidateEnum({ enum: PluginContext, name: 'PluginContext' })
|
||||
supportedContexts!: PluginContext[];
|
||||
schema!: JSONSchema | null;
|
||||
}
|
||||
|
||||
export class PluginActionResponseDto {
|
||||
id!: string;
|
||||
pluginId!: string;
|
||||
methodName!: string;
|
||||
title!: string;
|
||||
description!: string;
|
||||
|
||||
@ValidateEnum({ enum: PluginContext, name: 'PluginContext' })
|
||||
supportedContexts!: PluginContext[];
|
||||
schema!: JSONSchema | null;
|
||||
}
|
||||
|
||||
export class PluginInstallDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
manifestPath!: string;
|
||||
}
|
||||
|
||||
export type MapPlugin = {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
author: string;
|
||||
version: string;
|
||||
wasmPath: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
filters: PluginFilter[];
|
||||
actions: PluginAction[];
|
||||
};
|
||||
|
||||
export function mapPlugin(plugin: MapPlugin): PluginResponseDto {
|
||||
return {
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
title: plugin.title,
|
||||
description: plugin.description,
|
||||
author: plugin.author,
|
||||
version: plugin.version,
|
||||
createdAt: plugin.createdAt.toISOString(),
|
||||
updatedAt: plugin.updatedAt.toISOString(),
|
||||
filters: plugin.filters,
|
||||
actions: plugin.actions,
|
||||
};
|
||||
}
|
||||
@@ -91,4 +91,7 @@ export class QueuesResponseDto implements Record<QueueName, QueueResponseDto> {
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Ocr]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Workflow]!: QueueResponseDto;
|
||||
}
|
||||
|
||||
@@ -224,6 +224,12 @@ class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto>
|
||||
@IsObject()
|
||||
@Type(() => JobSettingsDto)
|
||||
[QueueName.Notification]!: JobSettingsDto;
|
||||
|
||||
@ApiProperty({ type: JobSettingsDto })
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@Type(() => JobSettingsDto)
|
||||
[QueueName.Workflow]!: JobSettingsDto;
|
||||
}
|
||||
|
||||
class SystemConfigLibraryScanDto {
|
||||
|
||||
120
server/src/dtos/workflow.dto.ts
Normal file
120
server/src/dtos/workflow.dto.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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';
|
||||
|
||||
export class WorkflowFilterItemDto {
|
||||
@IsUUID()
|
||||
filterId!: string;
|
||||
|
||||
@IsObject()
|
||||
@Optional()
|
||||
filterConfig?: FilterConfig;
|
||||
}
|
||||
|
||||
export class WorkflowActionItemDto {
|
||||
@IsUUID()
|
||||
actionId!: string;
|
||||
|
||||
@IsObject()
|
||||
@Optional()
|
||||
actionConfig?: ActionConfig;
|
||||
}
|
||||
|
||||
export class WorkflowCreateDto {
|
||||
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
|
||||
triggerType!: PluginTriggerType;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
description?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowFilterItemDto)
|
||||
filters!: WorkflowFilterItemDto[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowActionItemDto)
|
||||
actions!: WorkflowActionItemDto[];
|
||||
}
|
||||
|
||||
export class WorkflowUpdateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
description?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
enabled?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowFilterItemDto)
|
||||
@Optional()
|
||||
filters?: WorkflowFilterItemDto[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkflowActionItemDto)
|
||||
@Optional()
|
||||
actions?: WorkflowActionItemDto[];
|
||||
}
|
||||
|
||||
export class WorkflowResponseDto {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
triggerType!: PluginTriggerType;
|
||||
name!: string | null;
|
||||
description!: string;
|
||||
createdAt!: string;
|
||||
enabled!: boolean;
|
||||
filters!: WorkflowFilterResponseDto[];
|
||||
actions!: WorkflowActionResponseDto[];
|
||||
}
|
||||
|
||||
export class WorkflowFilterResponseDto {
|
||||
id!: string;
|
||||
workflowId!: string;
|
||||
filterId!: string;
|
||||
filterConfig!: FilterConfig | null;
|
||||
order!: number;
|
||||
}
|
||||
|
||||
export class WorkflowActionResponseDto {
|
||||
id!: string;
|
||||
workflowId!: string;
|
||||
actionId!: string;
|
||||
actionConfig!: ActionConfig | null;
|
||||
order!: number;
|
||||
}
|
||||
|
||||
export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterResponseDto {
|
||||
return {
|
||||
id: filter.id,
|
||||
workflowId: filter.workflowId,
|
||||
filterId: filter.filterId,
|
||||
filterConfig: filter.filterConfig,
|
||||
order: filter.order,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapWorkflowAction(action: WorkflowAction): WorkflowActionResponseDto {
|
||||
return {
|
||||
id: action.id,
|
||||
workflowId: action.workflowId,
|
||||
actionId: action.actionId,
|
||||
actionConfig: action.actionConfig,
|
||||
order: action.order,
|
||||
};
|
||||
}
|
||||
@@ -177,6 +177,11 @@ export enum Permission {
|
||||
PinCodeUpdate = 'pinCode.update',
|
||||
PinCodeDelete = 'pinCode.delete',
|
||||
|
||||
PluginCreate = 'plugin.create',
|
||||
PluginRead = 'plugin.read',
|
||||
PluginUpdate = 'plugin.update',
|
||||
PluginDelete = 'plugin.delete',
|
||||
|
||||
ServerAbout = 'server.about',
|
||||
ServerApkLinks = 'server.apkLinks',
|
||||
ServerStorage = 'server.storage',
|
||||
@@ -240,6 +245,11 @@ export enum Permission {
|
||||
UserProfileImageUpdate = 'userProfileImage.update',
|
||||
UserProfileImageDelete = 'userProfileImage.delete',
|
||||
|
||||
WorkflowCreate = 'workflow.create',
|
||||
WorkflowRead = 'workflow.read',
|
||||
WorkflowUpdate = 'workflow.update',
|
||||
WorkflowDelete = 'workflow.delete',
|
||||
|
||||
AdminUserCreate = 'adminUser.create',
|
||||
AdminUserRead = 'adminUser.read',
|
||||
AdminUserUpdate = 'adminUser.update',
|
||||
@@ -525,6 +535,7 @@ export enum QueueName {
|
||||
Notification = 'notifications',
|
||||
BackupDatabase = 'backupDatabase',
|
||||
Ocr = 'ocr',
|
||||
Workflow = 'workflow',
|
||||
}
|
||||
|
||||
export enum JobName {
|
||||
@@ -601,6 +612,9 @@ export enum JobName {
|
||||
// OCR
|
||||
OcrQueueAll = 'OcrQueueAll',
|
||||
Ocr = 'Ocr',
|
||||
|
||||
// Workflow
|
||||
WorkflowRun = 'WorkflowRun',
|
||||
}
|
||||
|
||||
export enum QueueCommand {
|
||||
@@ -793,6 +807,7 @@ export enum ApiTag {
|
||||
NotificationsAdmin = 'Notifications (admin)',
|
||||
Partners = 'Partners',
|
||||
People = 'People',
|
||||
Plugins = 'Plugins',
|
||||
Search = 'Search',
|
||||
Server = 'Server',
|
||||
Sessions = 'Sessions',
|
||||
@@ -807,4 +822,16 @@ export enum ApiTag {
|
||||
UsersAdmin = 'Users (admin)',
|
||||
Users = 'Users',
|
||||
Views = 'Views',
|
||||
Workflows = 'Workflows',
|
||||
}
|
||||
|
||||
export enum PluginContext {
|
||||
Asset = 'asset',
|
||||
Album = 'album',
|
||||
Person = 'person',
|
||||
}
|
||||
|
||||
export enum PluginTriggerType {
|
||||
AssetCreate = 'AssetCreate',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
37
server/src/plugins.ts
Normal file
37
server/src/plugins.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { PluginContext, PluginTriggerType } from 'src/enum';
|
||||
import { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
|
||||
export type PluginTrigger = {
|
||||
name: string;
|
||||
type: PluginTriggerType;
|
||||
description: string;
|
||||
context: PluginContext;
|
||||
schema: JSONSchema | null;
|
||||
};
|
||||
|
||||
export const pluginTriggers: PluginTrigger[] = [
|
||||
{
|
||||
name: 'Asset Uploaded',
|
||||
type: PluginTriggerType.AssetCreate,
|
||||
description: 'Triggered when a new asset is uploaded',
|
||||
context: PluginContext.Asset,
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
assetType: {
|
||||
type: 'string',
|
||||
description: 'Type of the asset',
|
||||
default: 'ALL',
|
||||
enum: ['Image', 'Video', 'All'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Person Recognized',
|
||||
type: PluginTriggerType.PersonRecognized,
|
||||
description: 'Triggered when a person is detected in an asset',
|
||||
context: PluginContext.Person,
|
||||
schema: null,
|
||||
},
|
||||
];
|
||||
@@ -243,3 +243,12 @@ from
|
||||
where
|
||||
"partner"."sharedById" in ($1)
|
||||
and "partner"."sharedWithId" = $2
|
||||
|
||||
-- AccessRepository.workflow.checkOwnerAccess
|
||||
select
|
||||
"workflow"."id"
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"workflow"."id" in ($1)
|
||||
and "workflow"."ownerId" = $2
|
||||
|
||||
159
server/src/queries/plugin.repository.sql
Normal file
159
server/src/queries/plugin.repository.sql
Normal file
@@ -0,0 +1,159 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- PluginRepository.getPlugin
|
||||
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"
|
||||
from
|
||||
"plugin"
|
||||
where
|
||||
"plugin"."id" = $1
|
||||
|
||||
-- PluginRepository.getPluginByName
|
||||
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"
|
||||
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"
|
||||
from
|
||||
"plugin"
|
||||
order by
|
||||
"plugin"."name"
|
||||
|
||||
-- PluginRepository.getFilter
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_filter"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- PluginRepository.getFiltersByPlugin
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_filter"
|
||||
where
|
||||
"pluginId" = $1
|
||||
|
||||
-- PluginRepository.getAction
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- PluginRepository.getActionsByPlugin
|
||||
select
|
||||
*
|
||||
from
|
||||
"plugin_action"
|
||||
where
|
||||
"pluginId" = $1
|
||||
68
server/src/queries/workflow.repository.sql
Normal file
68
server/src/queries/workflow.repository.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- WorkflowRepository.getWorkflow
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"id" = $1
|
||||
|
||||
-- WorkflowRepository.getWorkflowsByOwner
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"ownerId" = $1
|
||||
order by
|
||||
"name"
|
||||
|
||||
-- WorkflowRepository.getWorkflowsByTrigger
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"triggerType" = $1
|
||||
and "enabled" = $2
|
||||
|
||||
-- WorkflowRepository.getWorkflowByOwnerAndTrigger
|
||||
select
|
||||
*
|
||||
from
|
||||
"workflow"
|
||||
where
|
||||
"ownerId" = $1
|
||||
and "triggerType" = $2
|
||||
and "enabled" = $3
|
||||
|
||||
-- WorkflowRepository.deleteWorkflow
|
||||
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
|
||||
@@ -462,6 +462,26 @@ class TagAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class WorkflowAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, workflowIds: Set<string>) {
|
||||
if (workflowIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.select('workflow.id')
|
||||
.where('workflow.id', 'in', [...workflowIds])
|
||||
.where('workflow.ownerId', '=', userId)
|
||||
.execute()
|
||||
.then((workflows) => new Set(workflows.map((workflow) => workflow.id)));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AccessRepository {
|
||||
activity: ActivityAccess;
|
||||
@@ -476,6 +496,7 @@ export class AccessRepository {
|
||||
stack: StackAccess;
|
||||
tag: TagAccess;
|
||||
timeline: TimelineAccess;
|
||||
workflow: WorkflowAccess;
|
||||
|
||||
constructor(@InjectKysely() db: Kysely<DB>) {
|
||||
this.activity = new ActivityAccess(db);
|
||||
@@ -490,5 +511,6 @@ export class AccessRepository {
|
||||
this.stack = new StackAccess(db);
|
||||
this.tag = new TagAccess(db);
|
||||
this.timeline = new TimelineAccess(db);
|
||||
this.workflow = new WorkflowAccess(db);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface EnvData {
|
||||
root: string;
|
||||
indexHtml: string;
|
||||
};
|
||||
corePlugin: string;
|
||||
};
|
||||
|
||||
redis: RedisOptions;
|
||||
@@ -102,6 +103,11 @@ export interface EnvData {
|
||||
|
||||
workers: ImmichWorker[];
|
||||
|
||||
plugins: {
|
||||
enabled: boolean;
|
||||
installFolder?: string;
|
||||
};
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
@@ -304,6 +310,7 @@ const getEnv = (): EnvData => {
|
||||
root: folders.web,
|
||||
indexHtml: join(folders.web, 'index.html'),
|
||||
},
|
||||
corePlugin: join(buildFolder, 'corePlugin'),
|
||||
},
|
||||
|
||||
storage: {
|
||||
@@ -319,6 +326,11 @@ const getEnv = (): EnvData => {
|
||||
|
||||
workers,
|
||||
|
||||
plugins: {
|
||||
enabled: !!dto.IMMICH_PLUGINS_ENABLED,
|
||||
installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER,
|
||||
},
|
||||
|
||||
noColor: !!dto.NO_COLOR,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { compareSync, hash } from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { createHash, createPublicKey, createVerify, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
|
||||
@@ -57,4 +58,12 @@ export class CryptoRepository {
|
||||
randomBytesAsText(bytes: number) {
|
||||
return randomBytes(bytes).toString('base64').replaceAll(/\W/g, '');
|
||||
}
|
||||
|
||||
signJwt(payload: string | object | Buffer, secret: string, options?: jwt.SignOptions): string {
|
||||
return jwt.sign(payload, secret, { algorithm: 'HS256', ...options });
|
||||
}
|
||||
|
||||
verifyJwt<T = any>(token: string, secret: string): T {
|
||||
return jwt.verify(token, secret, { algorithms: ['HS256'] }) as T;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ClassConstructor } from 'class-transformer';
|
||||
import _ from 'lodash';
|
||||
import { Socket } from 'socket.io';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Asset } from 'src/database';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum';
|
||||
@@ -41,6 +42,7 @@ type EventMap = {
|
||||
AlbumInvite: [{ id: string; userId: string }];
|
||||
|
||||
// asset events
|
||||
AssetCreate: [{ asset: Asset }];
|
||||
AssetTag: [{ assetId: string }];
|
||||
AssetUntag: [{ assetId: string }];
|
||||
AssetHide: [{ assetId: string; userId: string }];
|
||||
|
||||
@@ -28,6 +28,7 @@ import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
@@ -46,6 +47,7 @@ import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
|
||||
export const repositories = [
|
||||
AccessRepository,
|
||||
@@ -78,6 +80,7 @@ export const repositories = [
|
||||
OcrRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
PluginRepository,
|
||||
ProcessRepository,
|
||||
SearchRepository,
|
||||
SessionRepository,
|
||||
@@ -96,4 +99,5 @@ export const repositories = [
|
||||
ViewRepository,
|
||||
VersionHistoryRepository,
|
||||
WebsocketRepository,
|
||||
WorkflowRepository,
|
||||
];
|
||||
|
||||
176
server/src/repositories/plugin.repository.ts
Normal file
176
server/src/repositories/plugin.repository.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { DB } from 'src/schema';
|
||||
|
||||
@Injectable()
|
||||
export class PluginRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
/**
|
||||
* Loads a plugin from a validated manifest file in a transaction.
|
||||
* This ensures all plugin, filter, and action operations are atomic.
|
||||
* @param manifest The validated plugin manifest
|
||||
* @param basePath The base directory path where the plugin is located
|
||||
*/
|
||||
async loadPlugin(manifest: PluginManifestDto, basePath: string) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
// Upsert the plugin
|
||||
const plugin = await tx
|
||||
.insertInto('plugin')
|
||||
.values({
|
||||
name: manifest.name,
|
||||
title: manifest.title,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
wasmPath: `${basePath}/${manifest.wasm.path}`,
|
||||
})
|
||||
.onConflict((oc) =>
|
||||
oc.column('name').doUpdateSet({
|
||||
title: manifest.title,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
version: manifest.version,
|
||||
wasmPath: `${basePath}/${manifest.wasm.path}`,
|
||||
}),
|
||||
)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const filters = manifest.filters
|
||||
? await tx
|
||||
.insertInto('plugin_filter')
|
||||
.values(
|
||||
manifest.filters.map((filter) => ({
|
||||
pluginId: plugin.id,
|
||||
methodName: filter.methodName,
|
||||
title: filter.title,
|
||||
description: filter.description,
|
||||
supportedContexts: filter.supportedContexts,
|
||||
schema: filter.schema,
|
||||
})),
|
||||
)
|
||||
.onConflict((oc) =>
|
||||
oc.column('methodName').doUpdateSet((eb) => ({
|
||||
pluginId: eb.ref('excluded.pluginId'),
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
supportedContexts: eb.ref('excluded.supportedContexts'),
|
||||
schema: eb.ref('excluded.schema'),
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
const actions = manifest.actions
|
||||
? await tx
|
||||
.insertInto('plugin_action')
|
||||
.values(
|
||||
manifest.actions.map((action) => ({
|
||||
pluginId: plugin.id,
|
||||
methodName: action.methodName,
|
||||
title: action.title,
|
||||
description: action.description,
|
||||
supportedContexts: action.supportedContexts,
|
||||
schema: action.schema,
|
||||
})),
|
||||
)
|
||||
.onConflict((oc) =>
|
||||
oc.column('methodName').doUpdateSet((eb) => ({
|
||||
pluginId: eb.ref('excluded.pluginId'),
|
||||
title: eb.ref('excluded.title'),
|
||||
description: eb.ref('excluded.description'),
|
||||
supportedContexts: eb.ref('excluded.supportedContexts'),
|
||||
schema: eb.ref('excluded.schema'),
|
||||
})),
|
||||
)
|
||||
.returningAll()
|
||||
.execute()
|
||||
: [];
|
||||
|
||||
return { plugin, filters, actions };
|
||||
});
|
||||
}
|
||||
|
||||
async readDirectory(path: string) {
|
||||
return readdir(path, { withFileTypes: true });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getPlugin(id: string) {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.where('plugin.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getPluginByName(name: string) {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.where('plugin.name', '=', name)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllPlugins() {
|
||||
return this.db
|
||||
.selectFrom('plugin')
|
||||
.select((eb) => [
|
||||
...columns.plugin,
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_filter').selectAll().whereRef('plugin_filter.pluginId', '=', 'plugin.id'),
|
||||
).as('filters'),
|
||||
jsonArrayFrom(
|
||||
eb.selectFrom('plugin_action').selectAll().whereRef('plugin_action.pluginId', '=', 'plugin.id'),
|
||||
).as('actions'),
|
||||
])
|
||||
.orderBy('plugin.name')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFilter(id: string) {
|
||||
return this.db.selectFrom('plugin_filter').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFiltersByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_filter').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAction(id: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getActionsByPlugin(pluginId: string) {
|
||||
return this.db.selectFrom('plugin_action').selectAll().where('pluginId', '=', pluginId).execute();
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,10 @@ export class StorageRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async readTextFile(filepath: string): Promise<string> {
|
||||
return fs.readFile(filepath, 'utf8');
|
||||
}
|
||||
|
||||
async checkFileExists(filepath: string, mode = constants.F_OK): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filepath, mode);
|
||||
|
||||
139
server/src/repositories/workflow.repository.ts
Normal file
139
server/src/repositories/workflow.repository.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { PluginTriggerType } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getWorkflow(id: string) {
|
||||
return this.db.selectFrom('workflow').selectAll().where('id', '=', id).executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getWorkflowsByOwner(ownerId: string) {
|
||||
return this.db.selectFrom('workflow').selectAll().where('ownerId', '=', ownerId).orderBy('name').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;
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async deleteWorkflow(id: string) {
|
||||
await this.db.deleteFrom('workflow').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFilters(workflowId: 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();
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ 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 { 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';
|
||||
@@ -69,6 +70,7 @@ 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 { Database, Extensions, Generated, Int8 } from 'src/sql-tools';
|
||||
|
||||
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
|
||||
@@ -125,6 +127,12 @@ export class ImmichDatabase {
|
||||
UserMetadataAuditTable,
|
||||
UserTable,
|
||||
VersionHistoryTable,
|
||||
PluginTable,
|
||||
PluginFilterTable,
|
||||
PluginActionTable,
|
||||
WorkflowTable,
|
||||
WorkflowFilterTable,
|
||||
WorkflowActionTable,
|
||||
];
|
||||
|
||||
functions = [
|
||||
@@ -231,4 +239,12 @@ export interface DB {
|
||||
user_metadata_audit: UserMetadataAuditTable;
|
||||
|
||||
version_history: VersionHistoryTable;
|
||||
|
||||
plugin: PluginTable;
|
||||
plugin_filter: PluginFilterTable;
|
||||
plugin_action: PluginActionTable;
|
||||
|
||||
workflow: WorkflowTable;
|
||||
workflow_filter: WorkflowFilterTable;
|
||||
workflow_action: WorkflowActionTable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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(),
|
||||
"name" character varying NOT NULL,
|
||||
"title" character varying NOT NULL,
|
||||
"description" character varying NOT NULL,
|
||||
"author" character varying NOT NULL,
|
||||
"version" character varying NOT NULL,
|
||||
"wasmPath" character varying NOT NULL,
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
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_filter" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"pluginId" uuid NOT NULL,
|
||||
"methodName" character varying NOT NULL,
|
||||
"title" character varying NOT NULL,
|
||||
"description" character varying NOT NULL,
|
||||
"supportedContexts" character varying[] NOT NULL,
|
||||
"schema" jsonb,
|
||||
CONSTRAINT "plugin_filter_pluginId_fkey" FOREIGN KEY ("pluginId") REFERENCES "plugin" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "plugin_filter_methodName_uq" UNIQUE ("methodName"),
|
||||
CONSTRAINT "plugin_filter_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "plugin_filter_supportedContexts_idx" ON "plugin_filter" USING gin ("supportedContexts");`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`CREATE INDEX "plugin_filter_pluginId_idx" ON "plugin_filter" ("pluginId");`.execute(db);
|
||||
await sql`CREATE INDEX "plugin_filter_methodName_idx" ON "plugin_filter" ("methodName");`.execute(db);
|
||||
await sql`CREATE TABLE "plugin_action" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"pluginId" uuid NOT NULL,
|
||||
"methodName" character varying NOT NULL,
|
||||
"title" character varying NOT NULL,
|
||||
"description" character varying NOT NULL,
|
||||
"supportedContexts" character varying[] NOT NULL,
|
||||
"schema" jsonb,
|
||||
CONSTRAINT "plugin_action_pluginId_fkey" FOREIGN KEY ("pluginId") REFERENCES "plugin" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "plugin_action_methodName_uq" UNIQUE ("methodName"),
|
||||
CONSTRAINT "plugin_action_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "plugin_action_supportedContexts_idx" ON "plugin_action" USING gin ("supportedContexts");`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`CREATE INDEX "plugin_action_pluginId_idx" ON "plugin_action" ("pluginId");`.execute(db);
|
||||
await sql`CREATE INDEX "plugin_action_methodName_idx" ON "plugin_action" ("methodName");`.execute(db);
|
||||
await sql`CREATE TABLE "workflow" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"ownerId" uuid NOT NULL,
|
||||
"triggerType" character varying NOT NULL,
|
||||
"name" character varying,
|
||||
"description" character varying NOT NULL,
|
||||
"createdAt" 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 TABLE "workflow_filter" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"workflowId" uuid NOT NULL,
|
||||
"filterId" uuid NOT NULL,
|
||||
"filterConfig" jsonb,
|
||||
"order" integer NOT NULL,
|
||||
CONSTRAINT "workflow_filter_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflow" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "workflow_filter_filterId_fkey" FOREIGN KEY ("filterId") REFERENCES "plugin_filter" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "workflow_filter_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_filter_filterId_idx" ON "workflow_filter" ("filterId");`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_filter_workflowId_order_idx" ON "workflow_filter" ("workflowId", "order");`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`CREATE INDEX "workflow_filter_workflowId_idx" ON "workflow_filter" ("workflowId");`.execute(db);
|
||||
await sql`CREATE TABLE "workflow_action" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"workflowId" uuid NOT NULL,
|
||||
"actionId" uuid NOT NULL,
|
||||
"actionConfig" jsonb,
|
||||
"order" integer NOT NULL,
|
||||
CONSTRAINT "workflow_action_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflow" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "workflow_action_actionId_fkey" FOREIGN KEY ("actionId") REFERENCES "plugin_action" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "workflow_action_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_action_actionId_idx" ON "workflow_action" ("actionId");`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_action_workflowId_order_idx" ON "workflow_action" ("workflowId", "order");`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`CREATE INDEX "workflow_action_workflowId_idx" ON "workflow_action" ("workflowId");`.execute(db);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_plugin_filter_supportedContexts_idx', '{"type":"index","name":"plugin_filter_supportedContexts_idx","sql":"CREATE INDEX \\"plugin_filter_supportedContexts_idx\\" ON \\"plugin_filter\\" (\\"supportedContexts\\") USING gin;"}'::jsonb);`.execute(
|
||||
db,
|
||||
);
|
||||
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_plugin_action_supportedContexts_idx', '{"type":"index","name":"plugin_action_supportedContexts_idx","sql":"CREATE INDEX \\"plugin_action_supportedContexts_idx\\" ON \\"plugin_action\\" (\\"supportedContexts\\") USING gin;"}'::jsonb);`.execute(
|
||||
db,
|
||||
);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE "workflow";`.execute(db);
|
||||
await sql`DROP TABLE "workflow_filter";`.execute(db);
|
||||
await sql`DROP TABLE "workflow_action";`.execute(db);
|
||||
|
||||
await sql`DROP TABLE "plugin";`.execute(db);
|
||||
await sql`DROP TABLE "plugin_filter";`.execute(db);
|
||||
await sql`DROP TABLE "plugin_action";`.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);
|
||||
}
|
||||
95
server/src/schema/tables/plugin.table.ts
Normal file
95
server/src/schema/tables/plugin.table.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { PluginContext } from 'src/enum';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
UpdateDateColumn,
|
||||
} from 'src/sql-tools';
|
||||
import type { JSONSchema } from 'src/types/plugin-schema.types';
|
||||
|
||||
@Table('plugin')
|
||||
export class PluginTable {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ index: true, unique: true })
|
||||
name!: string;
|
||||
|
||||
@Column()
|
||||
title!: string;
|
||||
|
||||
@Column()
|
||||
description!: string;
|
||||
|
||||
@Column()
|
||||
author!: string;
|
||||
|
||||
@Column()
|
||||
version!: string;
|
||||
|
||||
@Column()
|
||||
wasmPath!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@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;
|
||||
}
|
||||
78
server/src/schema/tables/workflow.table.ts
Normal file
78
server/src/schema/tables/workflow.table.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { PluginTriggerType } from 'src/enum';
|
||||
import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
} from 'src/sql-tools';
|
||||
import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types';
|
||||
|
||||
@Table('workflow')
|
||||
export class WorkflowTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
triggerType!: PluginTriggerType;
|
||||
|
||||
@Column({ nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@Column()
|
||||
description!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
enabled!: boolean;
|
||||
}
|
||||
|
||||
@Index({ columns: ['workflowId', 'order'] })
|
||||
@Index({ columns: ['filterId'] })
|
||||
@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' })
|
||||
filterId!: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
filterConfig!: FilterConfig | null;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
order!: number;
|
||||
}
|
||||
|
||||
@Index({ columns: ['workflowId', 'order'] })
|
||||
@Index({ columns: ['actionId'] })
|
||||
@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' })
|
||||
actionId!: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
actionConfig!: ActionConfig | null;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
order!: number;
|
||||
}
|
||||
@@ -426,6 +426,9 @@ export class AssetMediaService extends BaseService {
|
||||
}
|
||||
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
||||
await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
|
||||
|
||||
await this.eventRepository.emit('AssetCreate', { asset });
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } });
|
||||
|
||||
return asset;
|
||||
|
||||
@@ -35,6 +35,7 @@ import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
@@ -53,6 +54,7 @@ import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { AccessRequest, checkAccess, requireAccess } from 'src/utils/access';
|
||||
import { getConfig, updateConfig } from 'src/utils/config';
|
||||
@@ -88,6 +90,7 @@ export const BASE_SERVICE_DEPENDENCIES = [
|
||||
OcrRepository,
|
||||
PartnerRepository,
|
||||
PersonRepository,
|
||||
PluginRepository,
|
||||
ProcessRepository,
|
||||
SearchRepository,
|
||||
ServerInfoRepository,
|
||||
@@ -105,6 +108,8 @@ export const BASE_SERVICE_DEPENDENCIES = [
|
||||
UserRepository,
|
||||
VersionHistoryRepository,
|
||||
ViewRepository,
|
||||
WebsocketRepository,
|
||||
WorkflowRepository,
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
@@ -142,6 +147,7 @@ export class BaseService {
|
||||
protected ocrRepository: OcrRepository,
|
||||
protected partnerRepository: PartnerRepository,
|
||||
protected personRepository: PersonRepository,
|
||||
protected pluginRepository: PluginRepository,
|
||||
protected processRepository: ProcessRepository,
|
||||
protected searchRepository: SearchRepository,
|
||||
protected serverInfoRepository: ServerInfoRepository,
|
||||
@@ -160,6 +166,7 @@ export class BaseService {
|
||||
protected versionRepository: VersionHistoryRepository,
|
||||
protected viewRepository: ViewRepository,
|
||||
protected websocketRepository: WebsocketRepository,
|
||||
protected workflowRepository: WorkflowRepository,
|
||||
) {
|
||||
this.logger.setContext(this.constructor.name);
|
||||
this.storageCore = StorageCore.create(
|
||||
|
||||
@@ -23,6 +23,7 @@ import { NotificationService } from 'src/services/notification.service';
|
||||
import { OcrService } from 'src/services/ocr.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { PluginService } from 'src/services/plugin.service';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
@@ -43,6 +44,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 { WorkflowService } from 'src/services/workflow.service';
|
||||
|
||||
export const services = [
|
||||
ApiKeyService,
|
||||
@@ -70,6 +72,7 @@ export const services = [
|
||||
OcrService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
PluginService,
|
||||
QueueService,
|
||||
SearchService,
|
||||
ServerService,
|
||||
@@ -90,4 +93,5 @@ export const services = [
|
||||
UserService,
|
||||
VersionService,
|
||||
ViewService,
|
||||
WorkflowService,
|
||||
];
|
||||
|
||||
120
server/src/services/plugin-host.functions.ts
Normal file
120
server/src/services/plugin-host.functions.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
317
server/src/services/plugin.service.ts
Normal file
317
server/src/services/plugin.service.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
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 } from 'src/dtos/plugin.dto';
|
||||
import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
//
|
||||
// CRUD operations for plugins
|
||||
//
|
||||
async getAll(): Promise<PluginResponseDto[]> {
|
||||
const plugins = await this.pluginRepository.getAllPlugins();
|
||||
return plugins.map((plugin) => mapPlugin(plugin));
|
||||
}
|
||||
|
||||
async get(id: string): Promise<PluginResponseDto> {
|
||||
const plugin = await this.pluginRepository.getPlugin(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.enabled && plugins.installFolder) {
|
||||
await this.loadExternalPlugins(plugins.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.filterId);
|
||||
if (!filter) {
|
||||
this.logger.error(`Filter ${workflowFilter.filterId} 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.actionId);
|
||||
if (!action) {
|
||||
throw new Error(`Action ${workflowAction.actionId} 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ describe(QueueService.name, () => {
|
||||
it('should update concurrency', () => {
|
||||
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
|
||||
|
||||
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(16);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(17);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
|
||||
@@ -97,6 +97,7 @@ describe(QueueService.name, () => {
|
||||
[QueueName.Notification]: expectedJobStatus,
|
||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
||||
[QueueName.Ocr]: expectedJobStatus,
|
||||
[QueueName.Workflow]: expectedJobStatus,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
[QueueName.VideoConversion]: { concurrency: 1 },
|
||||
[QueueName.Notification]: { concurrency: 5 },
|
||||
[QueueName.Ocr]: { concurrency: 1 },
|
||||
[QueueName.Workflow]: { concurrency: 5 },
|
||||
},
|
||||
backup: {
|
||||
database: {
|
||||
|
||||
159
server/src/services/workflow.service.ts
Normal file
159
server/src/services/workflow.service.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { Workflow } from 'src/database';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
mapWorkflowAction,
|
||||
mapWorkflowFilter,
|
||||
WorkflowCreateDto,
|
||||
WorkflowResponseDto,
|
||||
WorkflowUpdateDto,
|
||||
} from 'src/dtos/workflow.dto';
|
||||
import { Permission, PluginContext, PluginTriggerType } from 'src/enum';
|
||||
import { pluginTriggers } from 'src/plugins';
|
||||
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowService extends BaseService {
|
||||
async create(auth: AuthDto, dto: WorkflowCreateDto): Promise<WorkflowResponseDto> {
|
||||
const trigger = this.getTriggerOrFail(dto.triggerType);
|
||||
|
||||
const filterInserts = await this.validateAndMapFilters(dto.filters, trigger.context);
|
||||
const actionInserts = await this.validateAndMapActions(dto.actions, trigger.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);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
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 trigger = this.getTriggerOrFail(workflow.triggerType);
|
||||
|
||||
const { filters, actions, ...workflowUpdate } = dto;
|
||||
const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context));
|
||||
const actionInserts = actions && (await this.validateAndMapActions(actions, trigger.context));
|
||||
|
||||
const updatedWorkflow = await this.workflowRepository.updateWorkflow(
|
||||
id,
|
||||
workflowUpdate,
|
||||
filterInserts,
|
||||
actionInserts,
|
||||
);
|
||||
|
||||
return this.mapWorkflow(updatedWorkflow);
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.WorkflowDelete, ids: [id] });
|
||||
await this.workflowRepository.deleteWorkflow(id);
|
||||
}
|
||||
|
||||
private async validateAndMapFilters(
|
||||
filters: Array<{ filterId: string; filterConfig?: any }>,
|
||||
requiredContext: PluginContext,
|
||||
) {
|
||||
for (const dto of filters) {
|
||||
const filter = await this.pluginRepository.getFilter(dto.filterId);
|
||||
if (!filter) {
|
||||
throw new BadRequestException(`Invalid filter ID: ${dto.filterId}`);
|
||||
}
|
||||
|
||||
if (!filter.supportedContexts.includes(requiredContext)) {
|
||||
throw new BadRequestException(
|
||||
`Filter "${filter.title}" does not support ${requiredContext} context. Supported contexts: ${filter.supportedContexts.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return filters.map((dto, index) => ({
|
||||
filterId: dto.filterId,
|
||||
filterConfig: dto.filterConfig || null,
|
||||
order: index,
|
||||
}));
|
||||
}
|
||||
|
||||
private async validateAndMapActions(
|
||||
actions: Array<{ actionId: string; actionConfig?: any }>,
|
||||
requiredContext: PluginContext,
|
||||
) {
|
||||
for (const dto of actions) {
|
||||
const action = await this.pluginRepository.getAction(dto.actionId);
|
||||
if (!action) {
|
||||
throw new BadRequestException(`Invalid action ID: ${dto.actionId}`);
|
||||
}
|
||||
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) => ({
|
||||
actionId: dto.actionId,
|
||||
actionConfig: dto.actionConfig || null,
|
||||
order: index,
|
||||
}));
|
||||
}
|
||||
|
||||
private getTriggerOrFail(triggerType: PluginTriggerType) {
|
||||
const trigger = pluginTriggers.find((t) => t.type === triggerType);
|
||||
if (!trigger) {
|
||||
throw new BadRequestException(`Invalid trigger type: ${triggerType}`);
|
||||
}
|
||||
return trigger;
|
||||
}
|
||||
|
||||
private async findOrFail(id: string) {
|
||||
const workflow = await this.workflowRepository.getWorkflow(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,5 +1,6 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { Asset } from 'src/database';
|
||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
ImageFormat,
|
||||
JobName,
|
||||
MemoryType,
|
||||
PluginTriggerType,
|
||||
QueueName,
|
||||
StorageFolder,
|
||||
SyncEntityType,
|
||||
@@ -263,6 +265,23 @@ 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> {
|
||||
id: string;
|
||||
type: T;
|
||||
event: WorkflowData[T];
|
||||
}
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
completed: number;
|
||||
@@ -374,7 +393,10 @@ export type JobItem =
|
||||
|
||||
// OCR
|
||||
| { name: JobName.OcrQueueAll; data: IBaseJob }
|
||||
| { name: JobName.Ocr; data: IEntityJob };
|
||||
| { name: JobName.Ocr; data: IEntityJob }
|
||||
|
||||
// Workflow
|
||||
| { name: JobName.WorkflowRun; data: IWorkflowJob };
|
||||
|
||||
export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number];
|
||||
|
||||
|
||||
35
server/src/types/plugin-schema.types.ts
Normal file
35
server/src/types/plugin-schema.types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -298,6 +298,12 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.WorkflowRead:
|
||||
case Permission.WorkflowUpdate:
|
||||
case Permission.WorkflowDelete: {
|
||||
return access.workflow.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { SessionRepository } from 'src/repositories/session.repository';
|
||||
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
||||
@@ -49,6 +50,7 @@ import { TagRepository } from 'src/repositories/tag.repository';
|
||||
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
@@ -380,6 +382,7 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case PluginRepository:
|
||||
case SearchRepository:
|
||||
case SessionRepository:
|
||||
case SharedLinkRepository:
|
||||
@@ -389,7 +392,8 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||
case SyncCheckpointRepository:
|
||||
case SystemMetadataRepository:
|
||||
case UserRepository:
|
||||
case VersionHistoryRepository: {
|
||||
case VersionHistoryRepository:
|
||||
case WorkflowRepository: {
|
||||
return new key(db);
|
||||
}
|
||||
|
||||
@@ -441,13 +445,15 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
case PersonRepository:
|
||||
case PluginRepository:
|
||||
case SessionRepository:
|
||||
case SyncRepository:
|
||||
case SyncCheckpointRepository:
|
||||
case SystemMetadataRepository:
|
||||
case UserRepository:
|
||||
case VersionHistoryRepository:
|
||||
case TagRepository: {
|
||||
case TagRepository:
|
||||
case WorkflowRepository: {
|
||||
return automock(key);
|
||||
}
|
||||
|
||||
|
||||
308
server/test/medium/specs/services/plugin.service.spec.ts
Normal file
308
server/test/medium/specs/services/plugin.service.spec.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PluginContext } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { PluginService } from 'src/services/plugin.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
let pluginRepo: PluginRepository;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PluginService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [PluginRepository, AccessRepository],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
pluginRepo = new PluginRepository(defaultDatabase);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await defaultDatabase.deleteFrom('plugin').execute();
|
||||
});
|
||||
|
||||
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([]);
|
||||
});
|
||||
|
||||
it('should return plugin without filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
{
|
||||
name: 'test-plugin',
|
||||
title: 'Test Plugin',
|
||||
description: 'A test plugin',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
wasm: { path: '/path/to/test.wasm' },
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0]).toMatchObject({
|
||||
id: result.plugin.id,
|
||||
name: 'test-plugin',
|
||||
description: 'A test plugin',
|
||||
author: 'Test Author',
|
||||
version: '1.0.0',
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return plugin with filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
{
|
||||
name: 'full-plugin',
|
||||
title: 'Full Plugin',
|
||||
description: 'A plugin with filters and actions',
|
||||
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: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0]).toMatchObject({
|
||||
id: result.plugin.id,
|
||||
name: 'full-plugin',
|
||||
filters: [
|
||||
{
|
||||
id: result.filters[0].id,
|
||||
pluginId: result.plugin.id,
|
||||
methodName: 'test-filter',
|
||||
title: 'Test Filter',
|
||||
description: 'A test filter',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: result.actions[0].id,
|
||||
pluginId: result.plugin.id,
|
||||
methodName: 'test-action',
|
||||
title: 'Test Action',
|
||||
description: 'A test action',
|
||||
supportedContexts: [PluginContext.Asset],
|
||||
schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return multiple plugins with their respective filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
|
||||
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[1].name).toBe('plugin-2');
|
||||
expect(plugins[1].filters).toHaveLength(0);
|
||||
expect(plugins[1].actions).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle plugin with multiple filters and actions', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
await pluginRepo.loadPlugin(
|
||||
{
|
||||
name: 'multi-plugin',
|
||||
title: 'Multi Plugin',
|
||||
description: 'Plugin with multiple items',
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/test/base/path',
|
||||
);
|
||||
|
||||
const plugins = await sut.getAll();
|
||||
|
||||
expect(plugins).toHaveLength(1);
|
||||
expect(plugins[0].filters).toHaveLength(2);
|
||||
expect(plugins[0].actions).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should throw error when plugin does not exist', async () => {
|
||||
const { sut } = setup();
|
||||
|
||||
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();
|
||||
|
||||
const result = await pluginRepo.loadPlugin(
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/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',
|
||||
title: 'Single Filter',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
id: result.actions[0].id,
|
||||
methodName: 'single-action',
|
||||
title: 'Single Action',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
697
server/test/medium/specs/services/workflow.service.spec.ts
Normal file
697
server/test/medium/specs/services/workflow.service.spec.ts
Normal file
@@ -0,0 +1,697 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PluginContext, PluginTriggerType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { WorkflowService } from 'src/services/workflow.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
import { getKyselyDB } from 'test/utils';
|
||||
|
||||
let defaultDatabase: Kysely<DB>;
|
||||
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(WorkflowService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [WorkflowRepository, PluginRepository, AccessRepository],
|
||||
mock: [LoggingRepository],
|
||||
});
|
||||
};
|
||||
|
||||
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(
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
'/plugins/test-core-plugin',
|
||||
);
|
||||
|
||||
testPluginId = result.plugin.id;
|
||||
testFilterId = result.filters[0].id;
|
||||
testActionId = result.actions[0].id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await defaultDatabase.deleteFrom('plugin').where('id', '=', testPluginId).execute();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a workflow without filters or actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.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,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a workflow with filters and actions', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow-with-relations',
|
||||
description: 'A test workflow with filters and actions',
|
||||
enabled: true,
|
||||
filters: [
|
||||
{
|
||||
filterId: testFilterId,
|
||||
filterConfig: { key: 'value' },
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
actionId: testActionId,
|
||||
actionConfig: { action: 'test' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(workflow).toMatchObject({
|
||||
id: expect.any(String),
|
||||
ownerId: user.id,
|
||||
triggerType: PluginTriggerType.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,
|
||||
filterId: testFilterId,
|
||||
filterConfig: { key: 'value' },
|
||||
order: 0,
|
||||
});
|
||||
|
||||
expect(workflow.actions).toHaveLength(1);
|
||||
expect(workflow.actions[0]).toMatchObject({
|
||||
id: expect.any(String),
|
||||
workflowId: workflow.id,
|
||||
actionId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-workflow',
|
||||
description: 'A workflow with invalid filter',
|
||||
enabled: true,
|
||||
filters: [
|
||||
{
|
||||
filterId: '66da82df-e424-4bf4-b6f3-5d8e71620dae',
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
await expect(
|
||||
sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'invalid-workflow',
|
||||
description: 'A workflow with invalid action',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [
|
||||
{
|
||||
actionId: '66da82df-e424-4bf4-b6f3-5d8e71620dae',
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
// 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: [{ filterId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
// 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: [{ actionId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'multi-step-workflow',
|
||||
description: 'A workflow with multiple filters and actions',
|
||||
enabled: true,
|
||||
filters: [
|
||||
{ filterId: testFilterId, filterConfig: { step: 1 } },
|
||||
{ filterId: testFilterId, filterConfig: { step: 2 } },
|
||||
],
|
||||
actions: [
|
||||
{ actionId: testActionId, actionConfig: { step: 1 } },
|
||||
{ actionId: testActionId, actionConfig: { step: 2 } },
|
||||
{ actionId: 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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all workflows for a user', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
const workflow1 = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'workflow-1',
|
||||
description: 'First workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const workflow2 = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'workflow-2',
|
||||
description: 'Second workflow',
|
||||
enabled: false,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const workflows = await sut.getAll(auth);
|
||||
|
||||
expect(workflows).toHaveLength(2);
|
||||
expect(workflows).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: workflow1.id, name: 'workflow-1' }),
|
||||
expect.objectContaining({ id: workflow2.id, name: 'workflow-2' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty array when user has no workflows', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
const workflows = await sut.getAll(auth);
|
||||
|
||||
expect(workflows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not return workflows from other users', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user: user1 } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth1 = { user: { id: user1.id } } as any;
|
||||
const auth2 = { user: { id: user2.id } } as any;
|
||||
|
||||
await sut.create(auth1, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'user1-workflow',
|
||||
description: 'User 1 workflow',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const user2Workflows = await sut.getAll(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 = { user: { id: user.id } } as any;
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'A test workflow',
|
||||
enabled: true,
|
||||
filters: [{ filterId: testFilterId, filterConfig: { key: 'value' } }],
|
||||
actions: [{ actionId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
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 = { user: { id: user1.id } } as any;
|
||||
const auth2 = { user: { id: user2.id } } as any;
|
||||
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ filterId: testFilterId, filterConfig: { old: 'config' } }],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
filters: [
|
||||
{ filterId: testFilterId, filterConfig: { new: 'config' } },
|
||||
{ filterId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [],
|
||||
actions: [{ actionId: testActionId, actionConfig: { old: 'config' } }],
|
||||
});
|
||||
|
||||
const updated = await sut.update(auth, created.id, {
|
||||
actions: [
|
||||
{ actionId: testActionId, actionConfig: { new: 'config' } },
|
||||
{ actionId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
const created = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ filterId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
await expect(
|
||||
sut.update(auth, 'non-existent-id', {
|
||||
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 = { user: { id: user1.id } } as any;
|
||||
const auth2 = { user: { id: user2.id } } as any;
|
||||
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
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: [{ filterId: 'invalid-filter-id', filterConfig: {} }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error when updating with invalid action', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
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: [{ actionId: 'invalid-action-id', actionConfig: {} }],
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('should delete a workflow', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = { user: { id: user.id } } as any;
|
||||
|
||||
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 = { user: { id: user.id } } as any;
|
||||
|
||||
const workflow = await sut.create(auth, {
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
name: 'test-workflow',
|
||||
description: 'Test',
|
||||
enabled: true,
|
||||
filters: [{ filterId: testFilterId, filterConfig: {} }],
|
||||
actions: [{ actionId: 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 = { user: { id: user.id } } as any;
|
||||
|
||||
await expect(sut.delete(auth, 'non-existent-id')).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 = { user: { id: user1.id } } as any;
|
||||
const auth2 = { user: { id: user2.id } } as any;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -65,5 +65,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
tag: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
workflow: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ const envData: EnvData = {
|
||||
root: '/build/www',
|
||||
indexHtml: '/build/www/index.html',
|
||||
},
|
||||
corePlugin: '/build/corePlugin',
|
||||
},
|
||||
|
||||
storage: {
|
||||
@@ -86,6 +87,11 @@ const envData: EnvData = {
|
||||
|
||||
workers: [ImmichWorker.Api, ImmichWorker.Microservices],
|
||||
|
||||
plugins: {
|
||||
enabled: true,
|
||||
installFolder: '/app/data/plugins',
|
||||
},
|
||||
|
||||
noColor: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -13,5 +13,7 @@ export const newCryptoRepositoryMock = (): Mocked<RepositoryInterface<CryptoRepo
|
||||
hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)),
|
||||
hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`),
|
||||
randomBytesAsText: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')),
|
||||
signJwt: vitest.fn().mockReturnValue('mock-jwt-token'),
|
||||
verifyJwt: vitest.fn().mockImplementation((token) => ({ verified: true, token })),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ export const newStorageRepositoryMock = (): Mocked<RepositoryInterface<StorageRe
|
||||
createZipStream: vitest.fn(),
|
||||
createReadStream: vitest.fn(),
|
||||
readFile: vitest.fn(),
|
||||
readTextFile: vitest.fn(),
|
||||
createFile: vitest.fn(),
|
||||
createWriteStream: vitest.fn(),
|
||||
createOrOverwriteFile: vitest.fn(),
|
||||
|
||||
@@ -44,6 +44,7 @@ import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { SearchRepository } from 'src/repositories/search.repository';
|
||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||
@@ -62,6 +63,7 @@ import { UserRepository } from 'src/repositories/user.repository';
|
||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||
import { ViewRepository } from 'src/repositories/view-repository';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository';
|
||||
import { WorkflowRepository } from 'src/repositories/workflow.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
@@ -235,6 +237,7 @@ export type ServiceOverrides = {
|
||||
oauth: OAuthRepository;
|
||||
partner: PartnerRepository;
|
||||
person: PersonRepository;
|
||||
plugin: PluginRepository;
|
||||
process: ProcessRepository;
|
||||
search: SearchRepository;
|
||||
serverInfo: ServerInfoRepository;
|
||||
@@ -253,6 +256,7 @@ export type ServiceOverrides = {
|
||||
versionHistory: VersionHistoryRepository;
|
||||
view: ViewRepository;
|
||||
websocket: WebsocketRepository;
|
||||
workflow: WorkflowRepository;
|
||||
};
|
||||
|
||||
type As<T> = T extends RepositoryInterface<infer U> ? U : never;
|
||||
@@ -308,6 +312,7 @@ export const newTestService = <T extends BaseService>(
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: automock(PersonRepository, { strict: false }),
|
||||
plugin: automock(PluginRepository, { strict: true }),
|
||||
process: automock(ProcessRepository),
|
||||
search: automock(SearchRepository, { strict: false }),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
@@ -330,6 +335,7 @@ export const newTestService = <T extends BaseService>(
|
||||
view: automock(ViewRepository),
|
||||
// eslint-disable-next-line no-sparse-arrays
|
||||
websocket: automock(WebsocketRepository, { args: [, loggerMock], strict: false }),
|
||||
workflow: automock(WorkflowRepository, { strict: true }),
|
||||
};
|
||||
|
||||
const sut = new Service(
|
||||
@@ -363,6 +369,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.ocr || (mocks.ocr as As<OcrRepository>),
|
||||
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
||||
overrides.person || (mocks.person as As<PersonRepository>),
|
||||
overrides.plugin || (mocks.plugin as As<PluginRepository>),
|
||||
overrides.process || (mocks.process as As<ProcessRepository>),
|
||||
overrides.search || (mocks.search as As<SearchRepository>),
|
||||
overrides.serverInfo || (mocks.serverInfo as As<ServerInfoRepository>),
|
||||
@@ -381,6 +388,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.versionHistory || (mocks.versionHistory as As<VersionHistoryRepository>),
|
||||
overrides.view || (mocks.view as As<ViewRepository>),
|
||||
overrides.websocket || (mocks.websocket as As<WebsocketRepository>),
|
||||
overrides.workflow || (mocks.workflow as As<WorkflowRepository>),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -162,6 +162,7 @@ export const getQueueName = derived(t, ($t) => {
|
||||
[QueueName.Notifications]: $t('notifications'),
|
||||
[QueueName.BackupDatabase]: $t('admin.backup_database'),
|
||||
[QueueName.Ocr]: $t('admin.machine_learning_ocr'),
|
||||
[QueueName.Workflow]: $t('workflow'),
|
||||
};
|
||||
|
||||
return names[name];
|
||||
|
||||
Reference in New Issue
Block a user