mirror of
https://github.com/immich-app/immich.git
synced 2026-07-18 05:34:18 +03:00
Compare commits
5 Commits
v3.0.2
...
feat/workf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024b7a0930 | ||
|
|
ba848dcd99 | ||
|
|
792c8f2691 | ||
|
|
3b79b471cb | ||
|
|
352c8086f9 |
@@ -1787,6 +1787,7 @@
|
||||
"restore_all": "Restore all",
|
||||
"restore_user": "Restore user",
|
||||
"restored_asset": "Restored asset",
|
||||
"result": "Result",
|
||||
"resume": "Resume",
|
||||
"resume_paused_jobs": "Resume {count, plural, one {# paused job} other {# paused jobs}}",
|
||||
"review_duplicates": "Review duplicates",
|
||||
@@ -2273,6 +2274,13 @@
|
||||
"workflow_info": "Workflow info",
|
||||
"workflow_json": "Workflow JSON",
|
||||
"workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.",
|
||||
"workflow_logging_completed": "Completed",
|
||||
"workflow_logging_disable": "Disable logging",
|
||||
"workflow_logging_disabled_description": "Logging is currently disabled for this workflow.",
|
||||
"workflow_logging_enable": "Enable logging",
|
||||
"workflow_logging_error_step": "Error on step #{step}",
|
||||
"workflow_logging_halted": "Halted",
|
||||
"workflow_logging_halted_step": "Halted on step #{step}",
|
||||
"workflow_name": "Workflow name",
|
||||
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
||||
"workflow_summary": "Workflow summary",
|
||||
|
||||
@@ -431,8 +431,8 @@
|
||||
"transcoding_realtime_description": "Permet au transcodage d'être réalisé en temps réel durant la diffusion du flux de la vidéo. Active la bascule automatique entre résolutions, mais peut entraîner une latence importante de lecture et des microcoupures en fonction des capacités du serveur.",
|
||||
"transcoding_realtime_enabled": "Activer la transcodification en temps réel",
|
||||
"transcoding_realtime_enabled_description": "Si désactivé, le serveur refusera de démarrer de nouvelles sessions de transcodifications en temps réel.",
|
||||
"transcoding_realtime_resolutions": "Définitions",
|
||||
"transcoding_realtime_resolutions_description": "Les définitions proposées pour le transcodage en temps réel. Des définitions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
|
||||
"transcoding_realtime_resolutions": "Résolutions",
|
||||
"transcoding_realtime_resolutions_description": "Les résolutions proposées pour le transcodage en temps réel. Des résolutions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
|
||||
"transcoding_realtime_video_codecs": "Codecs vidéo",
|
||||
"transcoding_realtime_video_codecs_description": "Les codecs vidéo proposés pour le transcodage en temps réel. Les clients choisissent la meilleure option qu'ils supportent durant la lecture. AV1 est plus efficace que HEVC, qui est plus efficace que H.264. En utilisant l'accélération matérielle, sélectionnez seulement les codecs que l'accélérateur peut encoder. En utilisant le transcodage logiciel, notez que H.264 est plus rapide que AV1, qui est plus rapide que HEVC.",
|
||||
"transcoding_reference_frames": "Trames de référence",
|
||||
|
||||
@@ -41,6 +41,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
'SyncAssetV1': {'isEdited': false},
|
||||
'ServerFeaturesDto': {'ocr': false, 'realtimeTranscoding': false},
|
||||
'MemoriesResponse': {'duration': 5},
|
||||
'WorkflowResponseDto': {'logging': false},
|
||||
};
|
||||
|
||||
void upgradeDto(dynamic value, String targetType) {
|
||||
|
||||
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
@@ -341,6 +341,7 @@ Class | Method | HTTP request | Description
|
||||
*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* | [**getWorkflowForShare**](doc//WorkflowsApi.md#getworkflowforshare) | **GET** /workflows/{id}/share | Retrieve a workflow
|
||||
*WorkflowsApi* | [**getWorkflowLogs**](doc//WorkflowsApi.md#getworkflowlogs) | **GET** /workflows/{id}/logs | Retrieve a workflow
|
||||
*WorkflowsApi* | [**getWorkflowTriggers**](doc//WorkflowsApi.md#getworkflowtriggers) | **GET** /workflows/triggers | List all workflow triggers
|
||||
*WorkflowsApi* | [**searchWorkflows**](doc//WorkflowsApi.md#searchworkflows) | **GET** /workflows | List all workflows
|
||||
*WorkflowsApi* | [**updateWorkflow**](doc//WorkflowsApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
|
||||
@@ -711,7 +712,10 @@ Class | Method | HTTP request | Description
|
||||
- [VideoCodec](doc//VideoCodec.md)
|
||||
- [VideoContainer](doc//VideoContainer.md)
|
||||
- [WorkflowCreateDto](doc//WorkflowCreateDto.md)
|
||||
- [WorkflowLogEntryDto](doc//WorkflowLogEntryDto.md)
|
||||
- [WorkflowLogEntryDtoLastStep](doc//WorkflowLogEntryDtoLastStep.md)
|
||||
- [WorkflowResponseDto](doc//WorkflowResponseDto.md)
|
||||
- [WorkflowResult](doc//WorkflowResult.md)
|
||||
- [WorkflowShareResponseDto](doc//WorkflowShareResponseDto.md)
|
||||
- [WorkflowShareStepDto](doc//WorkflowShareStepDto.md)
|
||||
- [WorkflowStepDto](doc//WorkflowStepDto.md)
|
||||
|
||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@@ -432,7 +432,10 @@ part 'model/version_check_state_response_dto.dart';
|
||||
part 'model/video_codec.dart';
|
||||
part 'model/video_container.dart';
|
||||
part 'model/workflow_create_dto.dart';
|
||||
part 'model/workflow_log_entry_dto.dart';
|
||||
part 'model/workflow_log_entry_dto_last_step.dart';
|
||||
part 'model/workflow_response_dto.dart';
|
||||
part 'model/workflow_result.dart';
|
||||
part 'model/workflow_share_response_dto.dart';
|
||||
part 'model/workflow_share_step_dto.dart';
|
||||
part 'model/workflow_step_dto.dart';
|
||||
|
||||
104
mobile/openapi/lib/api/workflows_api.dart
generated
104
mobile/openapi/lib/api/workflows_api.dart
generated
@@ -239,6 +239,95 @@ class WorkflowsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieve a workflow
|
||||
///
|
||||
/// Retrieve a workflow details without ids, default values, etc.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [DateTime] before:
|
||||
/// Filter by runs before a date/time
|
||||
///
|
||||
/// * [int] limit:
|
||||
/// Maximum number of logs
|
||||
///
|
||||
/// * [WorkflowResult] result:
|
||||
/// Filter by run result
|
||||
Future<Response> getWorkflowLogsWithHttpInfo(String id, { DateTime? before, int? limit, WorkflowResult? result, Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows/{id}/logs'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (before != null) {
|
||||
queryParams.addAll(_queryParams('', 'before', before));
|
||||
}
|
||||
if (limit != null) {
|
||||
queryParams.addAll(_queryParams('', 'limit', limit));
|
||||
}
|
||||
if (result != null) {
|
||||
queryParams.addAll(_queryParams('', 'result', result));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
abortTrigger: abortTrigger,
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieve a workflow
|
||||
///
|
||||
/// Retrieve a workflow details without ids, default values, etc.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [DateTime] before:
|
||||
/// Filter by runs before a date/time
|
||||
///
|
||||
/// * [int] limit:
|
||||
/// Maximum number of logs
|
||||
///
|
||||
/// * [WorkflowResult] result:
|
||||
/// Filter by run result
|
||||
Future<List<WorkflowLogEntryDto>?> getWorkflowLogs(String id, { DateTime? before, int? limit, WorkflowResult? result, Future<void>? abortTrigger, }) async {
|
||||
final response = await getWorkflowLogsWithHttpInfo(id, before: before, limit: limit, result: result, abortTrigger: abortTrigger,);
|
||||
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<WorkflowLogEntryDto>') as List)
|
||||
.cast<WorkflowLogEntryDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// List all workflow triggers
|
||||
///
|
||||
/// Retrieve a list of all available workflow triggers.
|
||||
@@ -308,12 +397,15 @@ class WorkflowsApi {
|
||||
/// * [String] id:
|
||||
/// Workflow ID
|
||||
///
|
||||
/// * [bool] logging:
|
||||
/// Workflow logs run results
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Workflow name
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
/// Workflow trigger type
|
||||
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
|
||||
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, bool? logging, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/workflows';
|
||||
|
||||
@@ -333,6 +425,9 @@ class WorkflowsApi {
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (logging != null) {
|
||||
queryParams.addAll(_queryParams('', 'logging', logging));
|
||||
}
|
||||
if (name != null) {
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
}
|
||||
@@ -370,13 +465,16 @@ class WorkflowsApi {
|
||||
/// * [String] id:
|
||||
/// Workflow ID
|
||||
///
|
||||
/// * [bool] logging:
|
||||
/// Workflow logs run results
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Workflow name
|
||||
///
|
||||
/// * [WorkflowTrigger] trigger:
|
||||
/// Workflow trigger type
|
||||
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
|
||||
final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, name: name, trigger: trigger, abortTrigger: abortTrigger,);
|
||||
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, bool? logging, String? name, WorkflowTrigger? trigger, Future<void>? abortTrigger, }) async {
|
||||
final response = await searchWorkflowsWithHttpInfo(description: description, enabled: enabled, id: id, logging: logging, name: name, trigger: trigger, abortTrigger: abortTrigger,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@@ -909,8 +909,14 @@ class ApiClient {
|
||||
return VideoContainerTypeTransformer().decode(value);
|
||||
case 'WorkflowCreateDto':
|
||||
return WorkflowCreateDto.fromJson(value);
|
||||
case 'WorkflowLogEntryDto':
|
||||
return WorkflowLogEntryDto.fromJson(value);
|
||||
case 'WorkflowLogEntryDtoLastStep':
|
||||
return WorkflowLogEntryDtoLastStep.fromJson(value);
|
||||
case 'WorkflowResponseDto':
|
||||
return WorkflowResponseDto.fromJson(value);
|
||||
case 'WorkflowResult':
|
||||
return WorkflowResultTypeTransformer().decode(value);
|
||||
case 'WorkflowShareResponseDto':
|
||||
return WorkflowShareResponseDto.fromJson(value);
|
||||
case 'WorkflowShareStepDto':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -214,6 +214,9 @@ String parameterToString(dynamic value) {
|
||||
if (value is VideoContainer) {
|
||||
return VideoContainerTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is WorkflowResult) {
|
||||
return WorkflowResultTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is WorkflowTrigger) {
|
||||
return WorkflowTriggerTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
3
mobile/openapi/lib/model/permission.dart
generated
3
mobile/openapi/lib/model/permission.dart
generated
@@ -172,6 +172,7 @@ class Permission {
|
||||
static const workflowPeriodRead = Permission._(r'workflow.read');
|
||||
static const workflowPeriodUpdate = Permission._(r'workflow.update');
|
||||
static const workflowPeriodDelete = Permission._(r'workflow.delete');
|
||||
static const workflowPeriodLogs = Permission._(r'workflow.logs');
|
||||
static const adminUserPeriodCreate = Permission._(r'adminUser.create');
|
||||
static const adminUserPeriodRead = Permission._(r'adminUser.read');
|
||||
static const adminUserPeriodUpdate = Permission._(r'adminUser.update');
|
||||
@@ -330,6 +331,7 @@ class Permission {
|
||||
workflowPeriodRead,
|
||||
workflowPeriodUpdate,
|
||||
workflowPeriodDelete,
|
||||
workflowPeriodLogs,
|
||||
adminUserPeriodCreate,
|
||||
adminUserPeriodRead,
|
||||
adminUserPeriodUpdate,
|
||||
@@ -523,6 +525,7 @@ class PermissionTypeTransformer {
|
||||
case r'workflow.read': return Permission.workflowPeriodRead;
|
||||
case r'workflow.update': return Permission.workflowPeriodUpdate;
|
||||
case r'workflow.delete': return Permission.workflowPeriodDelete;
|
||||
case r'workflow.logs': return Permission.workflowPeriodLogs;
|
||||
case r'adminUser.create': return Permission.adminUserPeriodCreate;
|
||||
case r'adminUser.read': return Permission.adminUserPeriodRead;
|
||||
case r'adminUser.update': return Permission.adminUserPeriodUpdate;
|
||||
|
||||
19
mobile/openapi/lib/model/workflow_create_dto.dart
generated
19
mobile/openapi/lib/model/workflow_create_dto.dart
generated
@@ -15,6 +15,7 @@ class WorkflowCreateDto {
|
||||
WorkflowCreateDto({
|
||||
this.description = const Optional.absent(),
|
||||
this.enabled = const Optional.absent(),
|
||||
this.logging = const Optional.absent(),
|
||||
this.name = const Optional.absent(),
|
||||
this.steps = const Optional.present(const []),
|
||||
required this.trigger,
|
||||
@@ -32,6 +33,15 @@ class WorkflowCreateDto {
|
||||
///
|
||||
Optional<bool?> enabled;
|
||||
|
||||
/// Workflow logs run results
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Optional<bool?> logging;
|
||||
|
||||
/// Workflow name
|
||||
Optional<String?> name;
|
||||
|
||||
@@ -43,6 +53,7 @@ class WorkflowCreateDto {
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowCreateDto &&
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
other.logging == logging &&
|
||||
other.name == name &&
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger;
|
||||
@@ -52,12 +63,13 @@ class WorkflowCreateDto {
|
||||
// ignore: unnecessary_parenthesis
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(logging == null ? 0 : logging!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(steps.hashCode) +
|
||||
(trigger.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowCreateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
|
||||
String toString() => 'WorkflowCreateDto[description=$description, enabled=$enabled, logging=$logging, name=$name, steps=$steps, trigger=$trigger]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -69,6 +81,10 @@ class WorkflowCreateDto {
|
||||
final value = this.enabled.value;
|
||||
json[r'enabled'] = value;
|
||||
}
|
||||
if (this.logging.isPresent) {
|
||||
final value = this.logging.value;
|
||||
json[r'logging'] = value;
|
||||
}
|
||||
if (this.name.isPresent) {
|
||||
final value = this.name.value;
|
||||
json[r'name'] = value;
|
||||
@@ -92,6 +108,7 @@ class WorkflowCreateDto {
|
||||
return WorkflowCreateDto(
|
||||
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
|
||||
enabled: json.containsKey(r'enabled') ? Optional.present(mapValueOfType<bool>(json, r'enabled')) : const Optional.absent(),
|
||||
logging: json.containsKey(r'logging') ? Optional.present(mapValueOfType<bool>(json, r'logging')) : const Optional.absent(),
|
||||
name: json.containsKey(r'name') ? Optional.present(mapValueOfType<String>(json, r'name')) : const Optional.absent(),
|
||||
steps: json.containsKey(r'steps') ? Optional.present(WorkflowStepDto.listFromJson(json[r'steps'])) : const Optional.absent(),
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
|
||||
150
mobile/openapi/lib/model/workflow_log_entry_dto.dart
generated
Normal file
150
mobile/openapi/lib/model/workflow_log_entry_dto.dart
generated
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// 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 WorkflowLogEntryDto {
|
||||
/// Returns a new [WorkflowLogEntryDto] instance.
|
||||
WorkflowLogEntryDto({
|
||||
required this.at,
|
||||
required this.id,
|
||||
this.lastStep = const Optional.absent(),
|
||||
required this.result,
|
||||
this.triggerDataId = const Optional.absent(),
|
||||
});
|
||||
|
||||
/// Workflow run date/time
|
||||
String at;
|
||||
|
||||
/// Workflow log entry ID
|
||||
String id;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Optional<WorkflowLogEntryDtoLastStep?> lastStep;
|
||||
|
||||
WorkflowResult result;
|
||||
|
||||
/// Workflow trigger data ID
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Optional<String?> triggerDataId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowLogEntryDto &&
|
||||
other.at == at &&
|
||||
other.id == id &&
|
||||
other.lastStep == lastStep &&
|
||||
other.result == result &&
|
||||
other.triggerDataId == triggerDataId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(at.hashCode) +
|
||||
(id.hashCode) +
|
||||
(lastStep == null ? 0 : lastStep!.hashCode) +
|
||||
(result.hashCode) +
|
||||
(triggerDataId == null ? 0 : triggerDataId!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowLogEntryDto[at=$at, id=$id, lastStep=$lastStep, result=$result, triggerDataId=$triggerDataId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'at'] = this.at;
|
||||
json[r'id'] = this.id;
|
||||
if (this.lastStep.isPresent) {
|
||||
final value = this.lastStep.value;
|
||||
json[r'lastStep'] = value;
|
||||
}
|
||||
json[r'result'] = this.result;
|
||||
if (this.triggerDataId.isPresent) {
|
||||
final value = this.triggerDataId.value;
|
||||
json[r'triggerDataId'] = value;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowLogEntryDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowLogEntryDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowLogEntryDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowLogEntryDto(
|
||||
at: mapValueOfType<String>(json, r'at')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
lastStep: json.containsKey(r'lastStep') ? Optional.present(WorkflowLogEntryDtoLastStep.fromJson(json[r'lastStep'])) : const Optional.absent(),
|
||||
result: WorkflowResult.fromJson(json[r'result'])!,
|
||||
triggerDataId: json.containsKey(r'triggerDataId') ? Optional.present(mapValueOfType<String>(json, r'triggerDataId')) : const Optional.absent(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowLogEntryDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowLogEntryDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowLogEntryDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowLogEntryDto> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowLogEntryDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowLogEntryDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowLogEntryDto-objects as value to a dart map
|
||||
static Map<String, List<WorkflowLogEntryDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowLogEntryDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowLogEntryDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'at',
|
||||
'id',
|
||||
'result',
|
||||
};
|
||||
}
|
||||
|
||||
112
mobile/openapi/lib/model/workflow_log_entry_dto_last_step.dart
generated
Normal file
112
mobile/openapi/lib/model/workflow_log_entry_dto_last_step.dart
generated
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// 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 WorkflowLogEntryDtoLastStep {
|
||||
/// Returns a new [WorkflowLogEntryDtoLastStep] instance.
|
||||
WorkflowLogEntryDtoLastStep({
|
||||
required this.index,
|
||||
required this.method,
|
||||
});
|
||||
|
||||
/// Index of the step in the workflow
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int index;
|
||||
|
||||
/// Method of the step
|
||||
String method;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowLogEntryDtoLastStep &&
|
||||
other.index == index &&
|
||||
other.method == method;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(index.hashCode) +
|
||||
(method.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowLogEntryDtoLastStep[index=$index, method=$method]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'index'] = this.index;
|
||||
json[r'method'] = this.method;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [WorkflowLogEntryDtoLastStep] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static WorkflowLogEntryDtoLastStep? fromJson(dynamic value) {
|
||||
upgradeDto(value, "WorkflowLogEntryDtoLastStep");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return WorkflowLogEntryDtoLastStep(
|
||||
index: mapValueOfType<int>(json, r'index')!,
|
||||
method: mapValueOfType<String>(json, r'method')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<WorkflowLogEntryDtoLastStep> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowLogEntryDtoLastStep>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowLogEntryDtoLastStep.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, WorkflowLogEntryDtoLastStep> mapFromJson(dynamic json) {
|
||||
final map = <String, WorkflowLogEntryDtoLastStep>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = WorkflowLogEntryDtoLastStep.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of WorkflowLogEntryDtoLastStep-objects as value to a dart map
|
||||
static Map<String, List<WorkflowLogEntryDtoLastStep>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<WorkflowLogEntryDtoLastStep>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = WorkflowLogEntryDtoLastStep.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'index',
|
||||
'method',
|
||||
};
|
||||
}
|
||||
|
||||
11
mobile/openapi/lib/model/workflow_response_dto.dart
generated
11
mobile/openapi/lib/model/workflow_response_dto.dart
generated
@@ -17,6 +17,7 @@ class WorkflowResponseDto {
|
||||
required this.description,
|
||||
required this.enabled,
|
||||
required this.id,
|
||||
required this.logging,
|
||||
required this.name,
|
||||
this.steps = const [],
|
||||
required this.trigger,
|
||||
@@ -35,6 +36,9 @@ class WorkflowResponseDto {
|
||||
/// Workflow ID
|
||||
String id;
|
||||
|
||||
/// Workflow logs run results
|
||||
bool logging;
|
||||
|
||||
/// Workflow name
|
||||
String? name;
|
||||
|
||||
@@ -52,6 +56,7 @@ class WorkflowResponseDto {
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
other.id == id &&
|
||||
other.logging == logging &&
|
||||
other.name == name &&
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger &&
|
||||
@@ -64,13 +69,14 @@ class WorkflowResponseDto {
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(id.hashCode) +
|
||||
(logging.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(steps.hashCode) +
|
||||
(trigger.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowResponseDto[createdAt=$createdAt, description=$description, enabled=$enabled, id=$id, name=$name, steps=$steps, trigger=$trigger, updatedAt=$updatedAt]';
|
||||
String toString() => 'WorkflowResponseDto[createdAt=$createdAt, description=$description, enabled=$enabled, id=$id, logging=$logging, name=$name, steps=$steps, trigger=$trigger, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -82,6 +88,7 @@ class WorkflowResponseDto {
|
||||
}
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'id'] = this.id;
|
||||
json[r'logging'] = this.logging;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
@@ -106,6 +113,7 @@ class WorkflowResponseDto {
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
logging: mapValueOfType<bool>(json, r'logging')!,
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
steps: WorkflowStepDto.listFromJson(json[r'steps']),
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
@@ -161,6 +169,7 @@ class WorkflowResponseDto {
|
||||
'description',
|
||||
'enabled',
|
||||
'id',
|
||||
'logging',
|
||||
'name',
|
||||
'steps',
|
||||
'trigger',
|
||||
|
||||
88
mobile/openapi/lib/model/workflow_result.dart
generated
Normal file
88
mobile/openapi/lib/model/workflow_result.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;
|
||||
|
||||
/// Workflow run result
|
||||
class WorkflowResult {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const WorkflowResult._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const completed = WorkflowResult._(r'completed');
|
||||
static const halted = WorkflowResult._(r'halted');
|
||||
static const error = WorkflowResult._(r'error');
|
||||
|
||||
/// List of all possible values in this [enum][WorkflowResult].
|
||||
static const values = <WorkflowResult>[
|
||||
completed,
|
||||
halted,
|
||||
error,
|
||||
];
|
||||
|
||||
static WorkflowResult? fromJson(dynamic value) => WorkflowResultTypeTransformer().decode(value);
|
||||
|
||||
static List<WorkflowResult> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <WorkflowResult>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = WorkflowResult.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [WorkflowResult] to String,
|
||||
/// and [decode] dynamic data back to [WorkflowResult].
|
||||
class WorkflowResultTypeTransformer {
|
||||
factory WorkflowResultTypeTransformer() => _instance ??= const WorkflowResultTypeTransformer._();
|
||||
|
||||
const WorkflowResultTypeTransformer._();
|
||||
|
||||
String encode(WorkflowResult data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a WorkflowResult.
|
||||
///
|
||||
/// 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.
|
||||
WorkflowResult? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'completed': return WorkflowResult.completed;
|
||||
case r'halted': return WorkflowResult.halted;
|
||||
case r'error': return WorkflowResult.error;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [WorkflowResultTypeTransformer] instance.
|
||||
static WorkflowResultTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
19
mobile/openapi/lib/model/workflow_update_dto.dart
generated
19
mobile/openapi/lib/model/workflow_update_dto.dart
generated
@@ -15,6 +15,7 @@ class WorkflowUpdateDto {
|
||||
WorkflowUpdateDto({
|
||||
this.description = const Optional.absent(),
|
||||
this.enabled = const Optional.absent(),
|
||||
this.logging = const Optional.absent(),
|
||||
this.name = const Optional.absent(),
|
||||
this.steps = const Optional.present(const []),
|
||||
this.trigger = const Optional.absent(),
|
||||
@@ -32,6 +33,15 @@ class WorkflowUpdateDto {
|
||||
///
|
||||
Optional<bool?> enabled;
|
||||
|
||||
/// Workflow logs run results
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
Optional<bool?> logging;
|
||||
|
||||
/// Workflow name
|
||||
Optional<String?> name;
|
||||
|
||||
@@ -49,6 +59,7 @@ class WorkflowUpdateDto {
|
||||
bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto &&
|
||||
other.description == description &&
|
||||
other.enabled == enabled &&
|
||||
other.logging == logging &&
|
||||
other.name == name &&
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.trigger == trigger;
|
||||
@@ -58,12 +69,13 @@ class WorkflowUpdateDto {
|
||||
// ignore: unnecessary_parenthesis
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(enabled == null ? 0 : enabled!.hashCode) +
|
||||
(logging == null ? 0 : logging!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(steps.hashCode) +
|
||||
(trigger == null ? 0 : trigger!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'WorkflowUpdateDto[description=$description, enabled=$enabled, name=$name, steps=$steps, trigger=$trigger]';
|
||||
String toString() => 'WorkflowUpdateDto[description=$description, enabled=$enabled, logging=$logging, name=$name, steps=$steps, trigger=$trigger]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -75,6 +87,10 @@ class WorkflowUpdateDto {
|
||||
final value = this.enabled.value;
|
||||
json[r'enabled'] = value;
|
||||
}
|
||||
if (this.logging.isPresent) {
|
||||
final value = this.logging.value;
|
||||
json[r'logging'] = value;
|
||||
}
|
||||
if (this.name.isPresent) {
|
||||
final value = this.name.value;
|
||||
json[r'name'] = value;
|
||||
@@ -101,6 +117,7 @@ class WorkflowUpdateDto {
|
||||
return WorkflowUpdateDto(
|
||||
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
|
||||
enabled: json.containsKey(r'enabled') ? Optional.present(mapValueOfType<bool>(json, r'enabled')) : const Optional.absent(),
|
||||
logging: json.containsKey(r'logging') ? Optional.present(mapValueOfType<bool>(json, r'logging')) : const Optional.absent(),
|
||||
name: json.containsKey(r'name') ? Optional.present(mapValueOfType<String>(json, r'name')) : const Optional.absent(),
|
||||
steps: json.containsKey(r'steps') ? Optional.present(WorkflowStepDto.listFromJson(json[r'steps'])) : const Optional.absent(),
|
||||
trigger: json.containsKey(r'trigger') ? Optional.present(WorkflowTrigger.fromJson(json[r'trigger'])) : const Optional.absent(),
|
||||
|
||||
@@ -15854,6 +15854,15 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "logging",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Workflow logs run results",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"required": false,
|
||||
@@ -16167,6 +16176,95 @@
|
||||
"x-immich-state": "Deprecated"
|
||||
}
|
||||
},
|
||||
"/workflows/{id}/logs": {
|
||||
"get": {
|
||||
"description": "Retrieve a workflow details without ids, default values, etc.",
|
||||
"operationId": "getWorkflowLogs",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "before",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by runs before a date/time",
|
||||
"schema": {
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Maximum number of logs",
|
||||
"schema": {
|
||||
"maximum": 9007199254740991,
|
||||
"exclusiveMinimum": true,
|
||||
"default": 50,
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "result",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by run result",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WorkflowResult"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WorkflowLogEntryDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Retrieve a workflow",
|
||||
"tags": [
|
||||
"Workflows"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3.0.0",
|
||||
"state": "Added"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "workflow.logs"
|
||||
}
|
||||
},
|
||||
"/workflows/{id}/share": {
|
||||
"get": {
|
||||
"description": "Retrieve a workflow details without ids, default values, etc.",
|
||||
@@ -21015,6 +21113,7 @@
|
||||
"workflow.read",
|
||||
"workflow.update",
|
||||
"workflow.delete",
|
||||
"workflow.logs",
|
||||
"adminUser.create",
|
||||
"adminUser.read",
|
||||
"adminUser.update",
|
||||
@@ -27855,6 +27954,10 @@
|
||||
"description": "Workflow enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"logging": {
|
||||
"description": "Workflow logs run results",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "Workflow name",
|
||||
"nullable": true,
|
||||
@@ -27876,6 +27979,56 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowLogEntryDto": {
|
||||
"properties": {
|
||||
"at": {
|
||||
"description": "Workflow run date/time",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Workflow log entry ID",
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
},
|
||||
"lastStep": {
|
||||
"description": "Last step ran, if the workflow ended early",
|
||||
"properties": {
|
||||
"index": {
|
||||
"description": "Index of the step in the workflow",
|
||||
"exclusiveMinimum": true,
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"method": {
|
||||
"description": "Method of the step",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"method",
|
||||
"index"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/components/schemas/WorkflowResult"
|
||||
},
|
||||
"triggerDataId": {
|
||||
"description": "Workflow trigger data ID",
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"at",
|
||||
"id",
|
||||
"result"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@@ -27897,6 +28050,10 @@
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
},
|
||||
"logging": {
|
||||
"description": "Workflow logs run results",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "Workflow name",
|
||||
"nullable": true,
|
||||
@@ -27923,6 +28080,7 @@
|
||||
"description",
|
||||
"enabled",
|
||||
"id",
|
||||
"logging",
|
||||
"name",
|
||||
"steps",
|
||||
"trigger",
|
||||
@@ -27930,6 +28088,15 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"WorkflowResult": {
|
||||
"description": "Workflow run result",
|
||||
"enum": [
|
||||
"completed",
|
||||
"halted",
|
||||
"error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"WorkflowShareResponseDto": {
|
||||
"properties": {
|
||||
"description": {
|
||||
@@ -28054,6 +28221,10 @@
|
||||
"description": "Workflow enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"logging": {
|
||||
"description": "Workflow logs run results",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "Workflow name",
|
||||
"nullable": true,
|
||||
|
||||
@@ -2791,6 +2791,8 @@ export type WorkflowResponseDto = {
|
||||
enabled: boolean;
|
||||
/** Workflow ID */
|
||||
id: string;
|
||||
/** Workflow logs run results */
|
||||
logging: boolean;
|
||||
/** Workflow name */
|
||||
name: string | null;
|
||||
/** Workflow steps */
|
||||
@@ -2805,6 +2807,8 @@ export type WorkflowCreateDto = {
|
||||
description?: string | null;
|
||||
/** Workflow enabled */
|
||||
enabled?: boolean;
|
||||
/** Workflow logs run results */
|
||||
logging?: boolean;
|
||||
/** Workflow name */
|
||||
name?: string | null;
|
||||
steps?: WorkflowStepDto[];
|
||||
@@ -2822,12 +2826,30 @@ export type WorkflowUpdateDto = {
|
||||
description?: string | null;
|
||||
/** Workflow enabled */
|
||||
enabled?: boolean;
|
||||
/** Workflow logs run results */
|
||||
logging?: boolean;
|
||||
/** Workflow name */
|
||||
name?: string | null;
|
||||
steps?: WorkflowStepDto[];
|
||||
/** Workflow trigger type */
|
||||
trigger?: WorkflowTrigger;
|
||||
};
|
||||
export type WorkflowLogEntryDto = {
|
||||
/** Workflow run date/time */
|
||||
at: string;
|
||||
/** Workflow log entry ID */
|
||||
id: string;
|
||||
/** Last step ran, if the workflow ended early */
|
||||
lastStep?: {
|
||||
/** Index of the step in the workflow */
|
||||
index: number;
|
||||
/** Method of the step */
|
||||
method: string;
|
||||
};
|
||||
result: WorkflowResult;
|
||||
/** Workflow trigger data ID */
|
||||
triggerDataId?: string;
|
||||
};
|
||||
export type WorkflowShareStepDto = {
|
||||
/** Step configuration */
|
||||
config: {
|
||||
@@ -6977,10 +6999,11 @@ export function getUniqueOriginalPaths(opts?: Oazapfts.RequestOpts) {
|
||||
/**
|
||||
* List all workflows
|
||||
*/
|
||||
export function searchWorkflows({ description, enabled, id, name, trigger }: {
|
||||
export function searchWorkflows({ description, enabled, id, logging, name, trigger }: {
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
logging?: boolean;
|
||||
name?: string;
|
||||
trigger?: WorkflowTrigger;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -6991,6 +7014,7 @@ export function searchWorkflows({ description, enabled, id, name, trigger }: {
|
||||
description,
|
||||
enabled,
|
||||
id,
|
||||
logging,
|
||||
name,
|
||||
trigger
|
||||
}))}`, {
|
||||
@@ -7063,6 +7087,26 @@ export function updateWorkflow({ id, workflowUpdateDto }: {
|
||||
body: workflowUpdateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Retrieve a workflow
|
||||
*/
|
||||
export function getWorkflowLogs({ before, id, limit, result }: {
|
||||
before?: string;
|
||||
id: string;
|
||||
limit?: number;
|
||||
result?: WorkflowResult;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: WorkflowLogEntryDto[];
|
||||
}>(`/workflows/${encodeURIComponent(id)}/logs${QS.query(QS.explode({
|
||||
before,
|
||||
limit,
|
||||
result
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve a workflow
|
||||
*/
|
||||
@@ -7310,6 +7354,7 @@ export enum Permission {
|
||||
WorkflowRead = "workflow.read",
|
||||
WorkflowUpdate = "workflow.update",
|
||||
WorkflowDelete = "workflow.delete",
|
||||
WorkflowLogs = "workflow.logs",
|
||||
AdminUserCreate = "adminUser.create",
|
||||
AdminUserRead = "adminUser.read",
|
||||
AdminUserUpdate = "adminUser.update",
|
||||
@@ -7686,6 +7731,11 @@ export enum AssetOrderBy {
|
||||
TakenAt = "takenAt",
|
||||
CreatedAt = "createdAt"
|
||||
}
|
||||
export enum WorkflowResult {
|
||||
Completed = "completed",
|
||||
Halted = "halted",
|
||||
Error = "error"
|
||||
}
|
||||
export enum ReleaseType {
|
||||
Major = "major",
|
||||
Premajor = "premajor",
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
WorkflowCreateDto,
|
||||
WorkflowGetLogsDto,
|
||||
WorkflowLogEntryDto,
|
||||
WorkflowResponseDto,
|
||||
WorkflowSearchDto,
|
||||
WorkflowShareResponseDto,
|
||||
@@ -113,4 +115,19 @@ export class WorkflowController {
|
||||
deleteWorkflow(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Get(':id/logs')
|
||||
@Authenticated({ permission: Permission.WorkflowLogs })
|
||||
@Endpoint({
|
||||
summary: 'Retrieve a workflow',
|
||||
description: 'Retrieve a workflow details without ids, default values, etc.',
|
||||
history: HistoryBuilder.v3(),
|
||||
})
|
||||
getWorkflowLogs(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: WorkflowGetLogsDto,
|
||||
): Promise<WorkflowLogEntryDto[]> {
|
||||
return this.service.getLogs(auth, id, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
||||
import { WorkflowResultSchema, WorkflowTriggerSchema, WorkflowTypeSchema } from 'src/enum';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const WorkflowTriggerResponseSchema = z
|
||||
@@ -17,6 +18,7 @@ const WorkflowSearchSchema = z
|
||||
name: z.string().optional().describe('Workflow name'),
|
||||
description: z.string().optional().describe('Workflow description'),
|
||||
enabled: z.boolean().optional().describe('Workflow enabled'),
|
||||
logging: z.boolean().optional().describe('Workflow logs run results'),
|
||||
})
|
||||
.meta({ id: 'WorkflowSearchDto' });
|
||||
|
||||
@@ -42,6 +44,7 @@ const WorkflowCreateSchema = z
|
||||
name: z.string().nullable().optional().describe('Workflow name'),
|
||||
description: z.string().nullable().optional().describe('Workflow description'),
|
||||
enabled: z.boolean().optional().describe('Workflow enabled'),
|
||||
logging: z.boolean().optional().describe('Workflow logs run results'),
|
||||
steps: z.array(WorkflowStepSchema).optional(),
|
||||
})
|
||||
.meta({ id: 'WorkflowCreateDto' });
|
||||
@@ -52,6 +55,7 @@ const WorkflowUpdateSchema = z
|
||||
name: z.string().nullable().optional().describe('Workflow name'),
|
||||
description: z.string().nullable().optional().describe('Workflow description'),
|
||||
enabled: z.boolean().optional().describe('Workflow enabled'),
|
||||
logging: z.boolean().optional().describe('Workflow logs run results'),
|
||||
steps: z.array(WorkflowStepSchema).optional(),
|
||||
})
|
||||
.meta({ id: 'WorkflowUpdateDto' });
|
||||
@@ -65,6 +69,7 @@ const WorkflowResponseSchema = z
|
||||
createdAt: z.string().describe('Creation date'),
|
||||
updatedAt: z.string().describe('Update date'),
|
||||
enabled: z.boolean().describe('Workflow enabled'),
|
||||
logging: z.boolean().describe('Workflow logs run results'),
|
||||
steps: z.array(WorkflowStepSchema).describe('Workflow steps'),
|
||||
})
|
||||
.meta({ id: 'WorkflowResponseDto' });
|
||||
@@ -78,12 +83,36 @@ const WorkflowShareResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'WorkflowShareResponseDto' });
|
||||
|
||||
const WorkflowLogEntrySchema = z
|
||||
.object({
|
||||
id: z.uuidv4().describe('Workflow log entry ID'),
|
||||
at: z.string().describe('Workflow run date/time'),
|
||||
result: WorkflowResultSchema.describe('Workflow run result'),
|
||||
triggerDataId: z.uuid().optional().describe('Workflow trigger data ID'),
|
||||
lastStep: z
|
||||
.object({
|
||||
method: z.string().describe('Method of the step'),
|
||||
index: z.int().positive().describe('Index of the step in the workflow'),
|
||||
})
|
||||
.optional()
|
||||
.describe('Last step ran, if the workflow ended early'),
|
||||
})
|
||||
.meta({ id: 'WorkflowLogEntryDto' });
|
||||
|
||||
const WorkflowGetLogsSchema = z.object({
|
||||
result: WorkflowResultSchema.optional().describe('Filter by run result'),
|
||||
before: isoDatetimeToDate.optional().describe('Filter by runs before a date/time'),
|
||||
limit: z.coerce.number().int().positive().default(50).describe('Maximum number of logs'),
|
||||
});
|
||||
|
||||
export class WorkflowTriggerResponseDto extends createZodDto(WorkflowTriggerResponseSchema) {}
|
||||
export class WorkflowSearchDto extends createZodDto(WorkflowSearchSchema) {}
|
||||
export class WorkflowCreateDto extends createZodDto(WorkflowCreateSchema) {}
|
||||
export class WorkflowUpdateDto extends createZodDto(WorkflowUpdateSchema) {}
|
||||
export class WorkflowResponseDto extends createZodDto(WorkflowResponseSchema) {}
|
||||
export class WorkflowShareResponseDto extends createZodDto(WorkflowShareResponseSchema) {}
|
||||
export class WorkflowLogEntryDto extends createZodDto(WorkflowLogEntrySchema) {}
|
||||
export class WorkflowGetLogsDto extends createZodDto(WorkflowGetLogsSchema) {}
|
||||
|
||||
type Workflow = {
|
||||
id: string;
|
||||
@@ -93,6 +122,7 @@ type Workflow = {
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
enabled: boolean;
|
||||
logging: boolean;
|
||||
};
|
||||
|
||||
type WorkflowStep = {
|
||||
@@ -107,6 +137,7 @@ export const mapWorkflow = (workflow: Workflow & { steps: WorkflowStep[] }): Wor
|
||||
id: workflow.id,
|
||||
enabled: workflow.enabled,
|
||||
trigger: workflow.trigger,
|
||||
logging: workflow.logging,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
createdAt: workflow.createdAt.toISOString(),
|
||||
|
||||
@@ -298,6 +298,7 @@ export enum Permission {
|
||||
WorkflowRead = 'workflow.read',
|
||||
WorkflowUpdate = 'workflow.update',
|
||||
WorkflowDelete = 'workflow.delete',
|
||||
WorkflowLogs = 'workflow.logs',
|
||||
|
||||
AdminUserCreate = 'adminUser.create',
|
||||
AdminUserRead = 'adminUser.read',
|
||||
@@ -1234,3 +1235,14 @@ export enum CalendarHeatmapType {
|
||||
Upload = 'Upload',
|
||||
Taken = 'Taken',
|
||||
}
|
||||
|
||||
export enum WorkflowResult {
|
||||
Completed = 'completed',
|
||||
Halted = 'halted',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export const WorkflowResultSchema = z
|
||||
.enum(WorkflowResult)
|
||||
.describe('Workflow run result')
|
||||
.meta({ id: 'WorkflowResult' });
|
||||
|
||||
@@ -9,6 +9,7 @@ select
|
||||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
"workflow"."logging",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@@ -41,6 +42,7 @@ select
|
||||
"workflow"."enabled",
|
||||
"workflow"."createdAt",
|
||||
"workflow"."updatedAt",
|
||||
"workflow"."logging",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@@ -69,6 +71,7 @@ select
|
||||
"workflow"."id",
|
||||
"workflow"."name",
|
||||
"workflow"."trigger",
|
||||
"workflow"."logging",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
@@ -96,6 +99,40 @@ where
|
||||
"id" = $2
|
||||
and "enabled" = $3
|
||||
|
||||
-- WorkflowRepository.getLogs
|
||||
select
|
||||
"workflow_log"."id",
|
||||
"workflow_log"."createdAt",
|
||||
"workflow_log"."halted",
|
||||
"workflow_log"."error",
|
||||
"workflow_log"."workflowId",
|
||||
"workflow_log"."workflowStepId",
|
||||
"workflow_log"."triggerDataId",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"plugin_method"."pluginId",
|
||||
"plugin_method"."name" as "methodName",
|
||||
"workflow_step"."order"
|
||||
from
|
||||
"workflow_step"
|
||||
inner join "plugin_method" on "plugin_method"."id" = "workflow_step"."pluginMethodId"
|
||||
where
|
||||
"workflow_step"."id" = "workflow_log"."workflowStepId"
|
||||
) as obj
|
||||
) as "step"
|
||||
from
|
||||
"workflow_log"
|
||||
where
|
||||
"workflow_log"."workflowId" = $1
|
||||
order by
|
||||
"workflow_log"."createdAt" desc
|
||||
limit
|
||||
$2
|
||||
|
||||
-- WorkflowRepository.delete
|
||||
delete from "workflow"
|
||||
where
|
||||
|
||||
@@ -4,8 +4,10 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { WorkflowSearchDto } from 'src/dtos/workflow.dto';
|
||||
import { WorkflowGetLogsDto, WorkflowSearchDto } from 'src/dtos/workflow.dto';
|
||||
import { WorkflowResult } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { WorkflowLogTable } from 'src/schema/tables/workflow-log.table';
|
||||
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
|
||||
import { WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
|
||||
@@ -26,6 +28,7 @@ export class WorkflowRepository {
|
||||
'workflow.enabled',
|
||||
'workflow.createdAt',
|
||||
'workflow.updatedAt',
|
||||
'workflow.logging',
|
||||
])
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
@@ -64,7 +67,7 @@ export class WorkflowRepository {
|
||||
getForWorkflowRun(id: string) {
|
||||
return this.db
|
||||
.selectFrom('workflow')
|
||||
.select(['workflow.id', 'workflow.name', 'workflow.trigger'])
|
||||
.select(['workflow.id', 'workflow.name', 'workflow.trigger', 'workflow.logging'])
|
||||
.select((eb) => [
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -97,6 +100,9 @@ export class WorkflowRepository {
|
||||
|
||||
update(id: string, dto: Updateable<WorkflowTable>, steps?: WorkflowStepUpsert[]) {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
if (dto.logging === false) {
|
||||
await tx.deleteFrom('workflow_log').where('workflowId', '=', id).execute();
|
||||
}
|
||||
if (Object.values(dto).some((prop) => prop !== undefined)) {
|
||||
await tx.updateTable('workflow').set(dto).where('id', '=', id).executeTakeFirstOrThrow();
|
||||
}
|
||||
@@ -104,6 +110,44 @@ export class WorkflowRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { result: undefined }] })
|
||||
getLogs(id: string, dto: WorkflowGetLogsDto) {
|
||||
return this.db
|
||||
.selectFrom('workflow_log')
|
||||
.select([
|
||||
'workflow_log.id',
|
||||
'workflow_log.createdAt',
|
||||
'workflow_log.halted',
|
||||
'workflow_log.error',
|
||||
'workflow_log.workflowId',
|
||||
'workflow_log.workflowStepId',
|
||||
'workflow_log.triggerDataId',
|
||||
])
|
||||
.where('workflow_log.workflowId', '=', id)
|
||||
.select((eb) => [
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom('workflow_step')
|
||||
.whereRef('workflow_step.id', '=', 'workflow_log.workflowStepId')
|
||||
.innerJoin('plugin_method', 'plugin_method.id', 'workflow_step.pluginMethodId')
|
||||
.select(['plugin_method.pluginId', 'plugin_method.name as methodName', 'workflow_step.order']),
|
||||
).as('step'),
|
||||
])
|
||||
.$if(dto.result === WorkflowResult.Error, (qb) => qb.where('workflow_log.error', '=', true))
|
||||
.$if(dto.result === WorkflowResult.Halted, (qb) => qb.where('workflow_log.halted', '=', true))
|
||||
.$if(dto.result === WorkflowResult.Completed, (qb) =>
|
||||
qb.where('workflow_log.halted', '=', false).where('workflow_log.error', '=', false),
|
||||
)
|
||||
.$if(dto.before !== undefined, (qb) => qb.where('workflow_log.createdAt', '<', dto.before!))
|
||||
.orderBy('workflow_log.createdAt', 'desc')
|
||||
.limit(dto.limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
log(dto: Insertable<WorkflowLogTable>) {
|
||||
return this.db.insertInto('workflow_log').values(dto).execute();
|
||||
}
|
||||
|
||||
async updateStep(id: string, dto: Updateable<WorkflowStepTable>) {
|
||||
await this.db.updateTable('workflow_step').where('workflow_step.id', '=', id).set(dto).execute();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ import {
|
||||
VideoStreamSessionTable,
|
||||
VideoStreamVariantTable,
|
||||
} from 'src/schema/tables/video-stream.table';
|
||||
import { WorkflowLogTable } from 'src/schema/tables/workflow-log.table';
|
||||
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
|
||||
import { WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
|
||||
@@ -277,4 +278,5 @@ export interface DB {
|
||||
|
||||
workflow: WorkflowTable;
|
||||
workflow_step: WorkflowStepTable;
|
||||
workflow_log: WorkflowLogTable;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "workflow" ADD "logging" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
await sql`CREATE TABLE "workflow_log" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
|
||||
"workflowId" uuid NOT NULL,
|
||||
"error" boolean NOT NULL,
|
||||
"halted" boolean NOT NULL,
|
||||
"workflowStepId" uuid,
|
||||
"triggerDataId" uuid,
|
||||
CONSTRAINT "workflow_log_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "workflow" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "workflow_log_workflowStepId_fkey" FOREIGN KEY ("workflowStepId") REFERENCES "workflow_step" ("id") ON UPDATE CASCADE ON DELETE SET NULL,
|
||||
CONSTRAINT "workflow_log_pkey" PRIMARY KEY ("id")
|
||||
);`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_log_workflowId_idx" ON "workflow_log" ("workflowId");`.execute(db);
|
||||
await sql`CREATE INDEX "workflow_log_workflowStepId_idx" ON "workflow_log" ("workflowStepId");`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "workflow" DROP COLUMN "logging";`.execute(db);
|
||||
await sql`DROP TABLE "workflow_log";`.execute(db);
|
||||
}
|
||||
35
server/src/schema/tables/workflow-log.table.ts
Normal file
35
server/src/schema/tables/workflow-log.table.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ForeignKeyColumn,
|
||||
Generated,
|
||||
PrimaryGeneratedColumn,
|
||||
Table,
|
||||
Timestamp,
|
||||
} from '@immich/sql-tools';
|
||||
import { WorkflowStepTable } from 'src/schema/tables/workflow-step.table';
|
||||
import { WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
|
||||
@Table('workflow_log')
|
||||
export class WorkflowLogTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
|
||||
@ForeignKeyColumn(() => WorkflowTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', index: true })
|
||||
workflowId!: string;
|
||||
|
||||
@Column({ type: 'boolean' })
|
||||
error!: boolean;
|
||||
|
||||
@Column({ type: 'boolean' })
|
||||
halted!: boolean;
|
||||
|
||||
@ForeignKeyColumn(() => WorkflowStepTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true })
|
||||
workflowStepId!: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
triggerDataId!: string | null;
|
||||
}
|
||||
@@ -41,4 +41,7 @@ export class WorkflowTable {
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
enabled!: Generated<boolean>;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
logging!: Generated<boolean>;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ const dummy = () => {
|
||||
};
|
||||
|
||||
type ExecuteOptions<T extends WorkflowType> = {
|
||||
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T> }>;
|
||||
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T>; entityId?: string }>;
|
||||
write: (auth: AuthDto, changes: WorkflowChanges<T>) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -331,6 +331,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
return {
|
||||
data: { asset } as any,
|
||||
authUserId: asset.ownerId,
|
||||
entityId: asset.id,
|
||||
};
|
||||
},
|
||||
write: async (auth, changes) => {
|
||||
@@ -398,11 +399,12 @@ export class WorkflowExecutionService extends BaseService {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { read, write } = handler;
|
||||
const readResult = await read(type);
|
||||
let data = readResult.data;
|
||||
for (const step of workflow.steps) {
|
||||
const { read, write } = handler;
|
||||
const readResult = await read(type);
|
||||
let data = readResult.data;
|
||||
|
||||
for (const step of workflow.steps) {
|
||||
try {
|
||||
const payload: WorkflowEventPayload<typeof type> = {
|
||||
trigger: workflow.trigger,
|
||||
type,
|
||||
@@ -453,14 +455,45 @@ export class WorkflowExecutionService extends BaseService {
|
||||
|
||||
const shouldContinue = result?.workflow?.continue ?? true;
|
||||
if (!shouldContinue) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (workflow.logging) {
|
||||
await this.workflowRepository.log({
|
||||
workflowId,
|
||||
error: false,
|
||||
halted: true,
|
||||
workflowStepId: step.id,
|
||||
triggerDataId: readResult.entityId,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.debug(`Workflow ${workflowId} executed successfully`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error executing workflow ${workflowId}:`, error);
|
||||
return JobStatus.Failed;
|
||||
this.logger.debug(`Workflow ${workflowId} stopped on step ${step.id}`);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error executing workflow ${workflowId}:`, error);
|
||||
|
||||
if (workflow.logging) {
|
||||
await this.workflowRepository.log({
|
||||
workflowId,
|
||||
error: true,
|
||||
halted: false,
|
||||
workflowStepId: step.id,
|
||||
triggerDataId: readResult.entityId,
|
||||
});
|
||||
}
|
||||
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
if (workflow.logging) {
|
||||
await this.workflowRepository.log({
|
||||
workflowId,
|
||||
error: false,
|
||||
halted: false,
|
||||
triggerDataId: readResult.entityId,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.debug(`Workflow ${workflowId} executed successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@ import {
|
||||
mapWorkflow,
|
||||
mapWorkflowShare,
|
||||
WorkflowCreateDto,
|
||||
WorkflowGetLogsDto,
|
||||
WorkflowLogEntryDto,
|
||||
WorkflowResponseDto,
|
||||
WorkflowSearchDto,
|
||||
WorkflowShareResponseDto,
|
||||
WorkflowTriggerResponseDto,
|
||||
WorkflowUpdateDto,
|
||||
} from 'src/dtos/workflow.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Permission, WorkflowResult } from 'src/enum';
|
||||
import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow';
|
||||
@@ -82,6 +84,23 @@ export class WorkflowService extends BaseService {
|
||||
await this.workflowRepository.delete(id);
|
||||
}
|
||||
|
||||
async getLogs(auth: AuthDto, id: string, dto: WorkflowGetLogsDto): Promise<WorkflowLogEntryDto[]> {
|
||||
await this.requireAccess({ auth, permission: Permission.WorkflowLogs, ids: [id] });
|
||||
const logs = await this.workflowRepository.getLogs(id, dto);
|
||||
return logs.map((entry) => ({
|
||||
id: entry.id,
|
||||
at: entry.createdAt.toISOString(),
|
||||
result: entry.error ? WorkflowResult.Error : entry.halted ? WorkflowResult.Halted : WorkflowResult.Completed,
|
||||
triggerDataId: entry.triggerDataId ?? undefined,
|
||||
lastStep: entry.step
|
||||
? {
|
||||
index: entry.step.order,
|
||||
method: `${entry.step.pluginId}#${entry.step.methodName}`,
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
private async resolveAndValidateSteps<T extends { method: string }>(steps: T[], trigger: WorkflowTrigger) {
|
||||
const methods = await this.pluginRepository.getForValidation();
|
||||
const results: Array<T & { pluginMethod: PluginMethodSearchResponse }> = [];
|
||||
|
||||
@@ -324,7 +324,8 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
|
||||
case Permission.WorkflowRead:
|
||||
case Permission.WorkflowUpdate:
|
||||
case Permission.WorkflowDelete: {
|
||||
case Permission.WorkflowDelete:
|
||||
case Permission.WorkflowLogs: {
|
||||
return access.workflow.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
|
||||
169
web/src/lib/modals/WorkflowLogsModal.svelte
Normal file
169
web/src/lib/modals/WorkflowLogsModal.svelte
Normal file
@@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { type WorkflowLogEntryDto, type WorkflowResponseDto, WorkflowResult, getWorkflowLogs } from '@immich/sdk';
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
Table,
|
||||
TableHeader,
|
||||
TableHeading,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
HStack,
|
||||
Icon,
|
||||
VStack,
|
||||
Button,
|
||||
Select,
|
||||
type SelectOption,
|
||||
} from '@immich/ui';
|
||||
import { mdiHistory, mdiOpenInNew } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleUpdateWorkflow } from '$lib/services/workflow.service';
|
||||
|
||||
type Props = {
|
||||
workflow: WorkflowResponseDto;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
let { workflow, onClose }: Props = $props();
|
||||
|
||||
let entries: WorkflowLogEntryDto[] = $state([]);
|
||||
let placeholder: HTMLElement | undefined = $state();
|
||||
let filter: WorkflowResult | undefined = $state();
|
||||
let before: string | undefined = $state();
|
||||
let hasNext = $state(true);
|
||||
let loading = $state(false);
|
||||
|
||||
const setLogging = (logging: boolean) =>
|
||||
handleUpdateWorkflow(workflow.id, { logging }).then((success) => {
|
||||
if (success) {
|
||||
workflow = { ...workflow, logging };
|
||||
reset();
|
||||
}
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
entries = [];
|
||||
hasNext = true;
|
||||
before = undefined;
|
||||
};
|
||||
|
||||
const setFilter = (option: SelectOption<WorkflowResult>) => {
|
||||
reset();
|
||||
filter = option.value;
|
||||
void getLogs();
|
||||
};
|
||||
|
||||
const getLogs = async () => {
|
||||
if (!hasNext || loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
const limit = 50;
|
||||
const results = await getWorkflowLogs({ id: workflow.id, result: filter, before, limit });
|
||||
entries.push(...results);
|
||||
if (results.length < limit) {
|
||||
hasNext = false;
|
||||
} else {
|
||||
before = results.at(-1)?.at;
|
||||
}
|
||||
loading = false;
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === placeholder);
|
||||
if (entry?.isIntersecting) {
|
||||
void getLogs();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (placeholder) {
|
||||
observer.disconnect();
|
||||
observer.observe(placeholder);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal title={$t('logs')} icon={mdiHistory} {onClose} size="medium">
|
||||
<ModalBody>
|
||||
{#if workflow.logging}
|
||||
<Table striped>
|
||||
<TableHeader>
|
||||
<TableHeading>{$t('date')}</TableHeading>
|
||||
<TableHeading>{$t('result')}</TableHeading>
|
||||
</TableHeader>
|
||||
<TableBody class="max-h-100">
|
||||
{#each entries as entry (entry.id)}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<HStack class="justify-center">
|
||||
<p>
|
||||
{DateTime.fromISO(entry.at).toLocaleString(DateTime.DATETIME_MED, {
|
||||
locale: $locale,
|
||||
})}
|
||||
</p>
|
||||
{#if entry.triggerDataId}
|
||||
<a href={Route.viewAsset({ id: entry.triggerDataId })} target="_blank">
|
||||
<Icon icon={mdiOpenInNew} size="20" class="text-primary" />
|
||||
</a>
|
||||
{/if}
|
||||
</HStack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HStack class="justify-center">
|
||||
{#if entry.result === WorkflowResult.Completed}
|
||||
<p class="rounded-full bg-green-700 px-3 py-1 text-xs text-white">
|
||||
{$t('workflow_logging_completed')}
|
||||
</p>
|
||||
{:else if entry.result === WorkflowResult.Halted}
|
||||
<p class="rounded-full bg-gray-600 px-3 py-1 text-xs text-white">
|
||||
{#if entry.lastStep}
|
||||
{$t('workflow_logging_halted_step', { values: { step: entry.lastStep.index + 1 } })}
|
||||
{:else}
|
||||
{$t('workflow_logging_halted')}
|
||||
{/if}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="rounded-full bg-red-500 px-3 py-1 text-xs text-white">
|
||||
{#if entry.lastStep}
|
||||
{$t('workflow_logging_error_step', { values: { step: entry.lastStep.index + 1 } })}
|
||||
{:else}
|
||||
{$t('error')}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</HStack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
{#if hasNext}
|
||||
<TableRow><TableCell><div bind:this={placeholder}>...</div></TableCell></TableRow>
|
||||
{/if}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div class="mt-5 flex gap-5">
|
||||
<Select
|
||||
onSelect={setFilter}
|
||||
class="flex-1"
|
||||
placeholder={$t('filter')}
|
||||
options={[
|
||||
{ value: WorkflowResult.Completed, label: $t('workflow_logging_completed') },
|
||||
{ value: WorkflowResult.Halted, label: $t('workflow_logging_halted') },
|
||||
{ value: WorkflowResult.Error, label: $t('error') },
|
||||
]}
|
||||
/>
|
||||
<Button class="flex-1" onclick={() => setLogging(false)}>{$t('workflow_logging_disable')}</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<VStack class="gap-5 py-5">
|
||||
<p class="text-md text-center">{$t('workflow_logging_disabled_description')}</p>
|
||||
<Button onclick={() => setLogging(true)}>{$t('workflow_logging_enable')}</Button>
|
||||
</VStack>
|
||||
{/if}
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
mdiDeleteOutline,
|
||||
mdiDownload,
|
||||
mdiFileDocumentMultipleOutline,
|
||||
mdiHistory,
|
||||
mdiPause,
|
||||
mdiPencil,
|
||||
mdiPlay,
|
||||
@@ -26,6 +27,7 @@ import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte';
|
||||
import WorkflowLogsModal from '$lib/modals/WorkflowLogsModal.svelte';
|
||||
import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard, downloadJson } from '$lib/utils';
|
||||
@@ -117,7 +119,13 @@ export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowRespo
|
||||
onAction: () => handleDeleteWorkflow(workflow),
|
||||
};
|
||||
|
||||
return { CopyJson, Download, Duplicate, ToggleEnabled, Edit, Delete };
|
||||
const Logs: ActionItem = {
|
||||
title: $t('check_logs'),
|
||||
icon: mdiHistory,
|
||||
onAction: () => modalManager.show(WorkflowLogsModal, { workflow }),
|
||||
};
|
||||
|
||||
return { CopyJson, Download, Duplicate, ToggleEnabled, Edit, Delete, Logs };
|
||||
};
|
||||
|
||||
export const getWorkflowShowSchemaAction = (
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
{:else}
|
||||
<div class="my-6 flex flex-col gap-3">
|
||||
{#each workflows as workflow (workflow.id)}
|
||||
{@const { ToggleEnabled, Duplicate, Edit, Delete } = getWorkflowActions($t, workflow)}
|
||||
{@const { ToggleEnabled, Duplicate, Logs, Edit, Delete } = getWorkflowActions($t, workflow)}
|
||||
|
||||
<Card class="group shadow-none transition-colors hover:border-primary">
|
||||
<CardHeader>
|
||||
@@ -120,6 +120,7 @@
|
||||
ToggleEnabled,
|
||||
Edit,
|
||||
Duplicate,
|
||||
Logs,
|
||||
getWorkflowShowSchemaAction($t, expandedIds.has(workflow.id), () => onToggleExpand(workflow.id)),
|
||||
MenuItemType.Divider,
|
||||
Delete,
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
|
||||
$effect(() => console.log(steps));
|
||||
|
||||
const { Download, Duplicate, CopyJson, Delete } = $derived(
|
||||
const { Download, Duplicate, CopyJson, Delete, Logs } = $derived(
|
||||
getWorkflowActions($t, { ...savedWorkflow, name, description, enabled, trigger, steps }),
|
||||
);
|
||||
</script>
|
||||
@@ -276,7 +276,7 @@
|
||||
{onClose}
|
||||
translations={{ close: $t('back') }}
|
||||
closeIcon={mdiArrowLeft}
|
||||
actions={[Duplicate, CopyJson, Download, Delete].map((item) => ({ ...item, color: undefined }))}
|
||||
actions={[Logs, Duplicate, CopyJson, Download, Delete].map((item) => ({ ...item, color: undefined }))}
|
||||
>
|
||||
<ControlBarHeader>
|
||||
<ControlBarTitle>{data.workflow.name}</ControlBarTitle>
|
||||
|
||||
Reference in New Issue
Block a user