diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 34845dcd9f..02daa8543d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -358,12 +358,11 @@ Class | Method | HTTP request | Description - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetEditAction](doc//AssetEditAction.md) - - [AssetEditActionCrop](doc//AssetEditActionCrop.md) - - [AssetEditActionListDto](doc//AssetEditActionListDto.md) - - [AssetEditActionListDtoEditsInner](doc//AssetEditActionListDtoEditsInner.md) - - [AssetEditActionMirror](doc//AssetEditActionMirror.md) - - [AssetEditActionRotate](doc//AssetEditActionRotate.md) - - [AssetEditsDto](doc//AssetEditsDto.md) + - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) + - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) + - [AssetEditActionItemResponseDto](doc//AssetEditActionItemResponseDto.md) + - [AssetEditsCreateDto](doc//AssetEditsCreateDto.md) + - [AssetEditsResponseDto](doc//AssetEditsResponseDto.md) - [AssetFaceCreateDto](doc//AssetFaceCreateDto.md) - [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md) - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 927ccae4cc..bfbe829d8d 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -97,12 +97,11 @@ part 'model/asset_copy_dto.dart'; part 'model/asset_delta_sync_dto.dart'; part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_edit_action.dart'; -part 'model/asset_edit_action_crop.dart'; -part 'model/asset_edit_action_list_dto.dart'; -part 'model/asset_edit_action_list_dto_edits_inner.dart'; -part 'model/asset_edit_action_mirror.dart'; -part 'model/asset_edit_action_rotate.dart'; -part 'model/asset_edits_dto.dart'; +part 'model/asset_edit_action_item_dto.dart'; +part 'model/asset_edit_action_item_dto_parameters.dart'; +part 'model/asset_edit_action_item_response_dto.dart'; +part 'model/asset_edits_create_dto.dart'; +part 'model/asset_edits_response_dto.dart'; part 'model/asset_face_create_dto.dart'; part 'model/asset_face_delete_dto.dart'; part 'model/asset_face_response_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 5fda01a594..a026b99028 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -421,14 +421,14 @@ class AssetsApi { /// /// * [String] id (required): /// - /// * [AssetEditActionListDto] assetEditActionListDto (required): - Future editAssetWithHttpInfo(String id, AssetEditActionListDto assetEditActionListDto,) async { + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/edits' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetEditActionListDto; + Object? postBody = assetEditsCreateDto; final queryParams = []; final headerParams = {}; @@ -456,9 +456,9 @@ class AssetsApi { /// /// * [String] id (required): /// - /// * [AssetEditActionListDto] assetEditActionListDto (required): - Future editAsset(String id, AssetEditActionListDto assetEditActionListDto,) async { - final response = await editAssetWithHttpInfo(id, assetEditActionListDto,); + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + final response = await editAssetWithHttpInfo(id, assetEditsCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -466,7 +466,7 @@ class AssetsApi { // 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), 'AssetEditsDto',) as AssetEditsDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsResponseDto',) as AssetEditsResponseDto; } return null; @@ -576,7 +576,7 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): - Future getAssetEdits(String id,) async { + Future getAssetEdits(String id,) async { final response = await getAssetEditsWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -585,7 +585,7 @@ class AssetsApi { // 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), 'AssetEditsDto',) as AssetEditsDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsResponseDto',) as AssetEditsResponseDto; } return null; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 33281f3be3..e703542b52 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -240,18 +240,16 @@ class ApiClient { return AssetDeltaSyncResponseDto.fromJson(value); case 'AssetEditAction': return AssetEditActionTypeTransformer().decode(value); - case 'AssetEditActionCrop': - return AssetEditActionCrop.fromJson(value); - case 'AssetEditActionListDto': - return AssetEditActionListDto.fromJson(value); - case 'AssetEditActionListDtoEditsInner': - return AssetEditActionListDtoEditsInner.fromJson(value); - case 'AssetEditActionMirror': - return AssetEditActionMirror.fromJson(value); - case 'AssetEditActionRotate': - return AssetEditActionRotate.fromJson(value); - case 'AssetEditsDto': - return AssetEditsDto.fromJson(value); + case 'AssetEditActionItemDto': + return AssetEditActionItemDto.fromJson(value); + case 'AssetEditActionItemDtoParameters': + return AssetEditActionItemDtoParameters.fromJson(value); + case 'AssetEditActionItemResponseDto': + return AssetEditActionItemResponseDto.fromJson(value); + case 'AssetEditsCreateDto': + return AssetEditsCreateDto.fromJson(value); + case 'AssetEditsResponseDto': + return AssetEditsResponseDto.fromJson(value); case 'AssetFaceCreateDto': return AssetFaceCreateDto.fromJson(value); case 'AssetFaceDeleteDto': diff --git a/mobile/openapi/lib/model/asset_edit_action_crop.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart similarity index 59% rename from mobile/openapi/lib/model/asset_edit_action_crop.dart rename to mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7672ed825b..7829de4bd5 100644 --- a/mobile/openapi/lib/model/asset_edit_action_crop.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AssetEditActionCrop { - /// Returns a new [AssetEditActionCrop] instance. - AssetEditActionCrop({ +class AssetEditActionItemDto { + /// Returns a new [AssetEditActionItemDto] instance. + AssetEditActionItemDto({ required this.action, required this.parameters, }); @@ -20,10 +20,10 @@ class AssetEditActionCrop { /// Type of edit action to perform AssetEditAction action; - CropParameters parameters; + AssetEditActionItemDtoParameters parameters; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionCrop && + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && other.action == action && other.parameters == parameters; @@ -34,7 +34,7 @@ class AssetEditActionCrop { (parameters.hashCode); @override - String toString() => 'AssetEditActionCrop[action=$action, parameters=$parameters]'; + String toString() => 'AssetEditActionItemDto[action=$action, parameters=$parameters]'; Map toJson() { final json = {}; @@ -43,27 +43,27 @@ class AssetEditActionCrop { return json; } - /// Returns a new [AssetEditActionCrop] instance and imports its values from + /// Returns a new [AssetEditActionItemDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionCrop? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionCrop"); + static AssetEditActionItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionCrop( + return AssetEditActionItemDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: CropParameters.fromJson(json[r'parameters'])!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionCrop.fromJson(row); + final value = AssetEditActionItemDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +72,12 @@ class AssetEditActionCrop { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionCrop.fromJson(entry.value); + final value = AssetEditActionItemDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +86,14 @@ class AssetEditActionCrop { return map; } - // maps a json object with a list of AssetEditActionCrop-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditActionItemDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionCrop.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditActionItemDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart new file mode 100644 index 0000000000..fc67aa022f --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -0,0 +1,153 @@ +// +// 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 AssetEditActionItemDtoParameters { + /// Returns a new [AssetEditActionItemDtoParameters] instance. + AssetEditActionItemDtoParameters({ + required this.height, + required this.width, + required this.x, + required this.y, + required this.angle, + required this.axis, + }); + + /// Height of the crop + /// + /// Minimum value: 1 + num height; + + /// Width of the crop + /// + /// Minimum value: 1 + num width; + + /// Top-Left X coordinate of crop + /// + /// Minimum value: 0 + num x; + + /// Top-Left Y coordinate of crop + /// + /// Minimum value: 0 + num y; + + /// Rotation angle in degrees + num angle; + + /// Axis to mirror along + MirrorAxis axis; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDtoParameters && + other.height == height && + other.width == width && + other.x == x && + other.y == y && + other.angle == angle && + other.axis == axis; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (height.hashCode) + + (width.hashCode) + + (x.hashCode) + + (y.hashCode) + + (angle.hashCode) + + (axis.hashCode); + + @override + String toString() => 'AssetEditActionItemDtoParameters[height=$height, width=$width, x=$x, y=$y, angle=$angle, axis=$axis]'; + + Map toJson() { + final json = {}; + json[r'height'] = this.height; + json[r'width'] = this.width; + json[r'x'] = this.x; + json[r'y'] = this.y; + json[r'angle'] = this.angle; + json[r'axis'] = this.axis; + return json; + } + + /// Returns a new [AssetEditActionItemDtoParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionItemDtoParameters? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDtoParameters"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionItemDtoParameters( + height: num.parse('${json[r'height']}'), + width: num.parse('${json[r'width']}'), + x: num.parse('${json[r'x']}'), + y: num.parse('${json[r'y']}'), + angle: num.parse('${json[r'angle']}'), + axis: MirrorAxis.fromJson(json[r'axis'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetEditActionItemDtoParameters.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetEditActionItemDtoParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionItemDtoParameters-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetEditActionItemDtoParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'height', + 'width', + 'x', + 'y', + 'angle', + 'axis', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart similarity index 54% rename from mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart rename to mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index 00c9be2381..a23a1ef5f3 100644 --- a/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -10,60 +10,67 @@ part of openapi.api; -class AssetEditActionListDtoEditsInner { - /// Returns a new [AssetEditActionListDtoEditsInner] instance. - AssetEditActionListDtoEditsInner({ +class AssetEditActionItemResponseDto { + /// Returns a new [AssetEditActionItemResponseDto] instance. + AssetEditActionItemResponseDto({ required this.action, + required this.id, required this.parameters, }); /// Type of edit action to perform AssetEditAction action; - MirrorParameters parameters; + String id; + + AssetEditActionItemDtoParameters parameters; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDtoEditsInner && + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemResponseDto && other.action == action && + other.id == id && other.parameters == parameters; @override int get hashCode => // ignore: unnecessary_parenthesis (action.hashCode) + + (id.hashCode) + (parameters.hashCode); @override - String toString() => 'AssetEditActionListDtoEditsInner[action=$action, parameters=$parameters]'; + String toString() => 'AssetEditActionItemResponseDto[action=$action, id=$id, parameters=$parameters]'; Map toJson() { final json = {}; json[r'action'] = this.action; + json[r'id'] = this.id; json[r'parameters'] = this.parameters; return json; } - /// Returns a new [AssetEditActionListDtoEditsInner] instance and imports its values from + /// Returns a new [AssetEditActionItemResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionListDtoEditsInner? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionListDtoEditsInner"); + static AssetEditActionItemResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemResponseDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionListDtoEditsInner( + return AssetEditActionItemResponseDto( action: AssetEditAction.fromJson(json[r'action'])!, - parameters: MirrorParameters.fromJson(json[r'parameters'])!, + id: mapValueOfType(json, r'id')!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionListDtoEditsInner.fromJson(row); + final value = AssetEditActionItemResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -72,12 +79,12 @@ class AssetEditActionListDtoEditsInner { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionListDtoEditsInner.fromJson(entry.value); + final value = AssetEditActionItemResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -86,14 +93,14 @@ class AssetEditActionListDtoEditsInner { return map; } - // maps a json object with a list of AssetEditActionListDtoEditsInner-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditActionItemResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionListDtoEditsInner.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditActionItemResponseDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -102,6 +109,7 @@ class AssetEditActionListDtoEditsInner { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'action', + 'id', 'parameters', }; } diff --git a/mobile/openapi/lib/model/asset_edit_action_mirror.dart b/mobile/openapi/lib/model/asset_edit_action_mirror.dart deleted file mode 100644 index aef98fc1a8..0000000000 --- a/mobile/openapi/lib/model/asset_edit_action_mirror.dart +++ /dev/null @@ -1,108 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetEditActionMirror { - /// Returns a new [AssetEditActionMirror] instance. - AssetEditActionMirror({ - required this.action, - required this.parameters, - }); - - /// Type of edit action to perform - AssetEditAction action; - - MirrorParameters parameters; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionMirror && - other.action == action && - other.parameters == parameters; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (action.hashCode) + - (parameters.hashCode); - - @override - String toString() => 'AssetEditActionMirror[action=$action, parameters=$parameters]'; - - Map toJson() { - final json = {}; - json[r'action'] = this.action; - json[r'parameters'] = this.parameters; - return json; - } - - /// Returns a new [AssetEditActionMirror] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetEditActionMirror? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionMirror"); - if (value is Map) { - final json = value.cast(); - - return AssetEditActionMirror( - action: AssetEditAction.fromJson(json[r'action'])!, - parameters: MirrorParameters.fromJson(json[r'parameters'])!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetEditActionMirror.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetEditActionMirror.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetEditActionMirror-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetEditActionMirror.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'action', - 'parameters', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_rotate.dart b/mobile/openapi/lib/model/asset_edit_action_rotate.dart deleted file mode 100644 index 302e6a0ce6..0000000000 --- a/mobile/openapi/lib/model/asset_edit_action_rotate.dart +++ /dev/null @@ -1,108 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class AssetEditActionRotate { - /// Returns a new [AssetEditActionRotate] instance. - AssetEditActionRotate({ - required this.action, - required this.parameters, - }); - - /// Type of edit action to perform - AssetEditAction action; - - RotateParameters parameters; - - @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionRotate && - other.action == action && - other.parameters == parameters; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (action.hashCode) + - (parameters.hashCode); - - @override - String toString() => 'AssetEditActionRotate[action=$action, parameters=$parameters]'; - - Map toJson() { - final json = {}; - json[r'action'] = this.action; - json[r'parameters'] = this.parameters; - return json; - } - - /// Returns a new [AssetEditActionRotate] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AssetEditActionRotate? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionRotate"); - if (value is Map) { - final json = value.cast(); - - return AssetEditActionRotate( - action: AssetEditAction.fromJson(json[r'action'])!, - parameters: RotateParameters.fromJson(json[r'parameters'])!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetEditActionRotate.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = AssetEditActionRotate.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AssetEditActionRotate-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = AssetEditActionRotate.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'action', - 'parameters', - }; -} - diff --git a/mobile/openapi/lib/model/asset_edit_action_list_dto.dart b/mobile/openapi/lib/model/asset_edits_create_dto.dart similarity index 57% rename from mobile/openapi/lib/model/asset_edit_action_list_dto.dart rename to mobile/openapi/lib/model/asset_edits_create_dto.dart index e843c66e8f..9f6fc66904 100644 --- a/mobile/openapi/lib/model/asset_edit_action_list_dto.dart +++ b/mobile/openapi/lib/model/asset_edits_create_dto.dart @@ -10,17 +10,17 @@ part of openapi.api; -class AssetEditActionListDto { - /// Returns a new [AssetEditActionListDto] instance. - AssetEditActionListDto({ +class AssetEditsCreateDto { + /// Returns a new [AssetEditsCreateDto] instance. + AssetEditsCreateDto({ this.edits = const [], }); /// List of edit actions to apply (crop, rotate, or mirror) - List edits; + List edits; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDto && + bool operator ==(Object other) => identical(this, other) || other is AssetEditsCreateDto && _deepEquality.equals(other.edits, edits); @override @@ -29,7 +29,7 @@ class AssetEditActionListDto { (edits.hashCode); @override - String toString() => 'AssetEditActionListDto[edits=$edits]'; + String toString() => 'AssetEditsCreateDto[edits=$edits]'; Map toJson() { final json = {}; @@ -37,26 +37,26 @@ class AssetEditActionListDto { return json; } - /// Returns a new [AssetEditActionListDto] instance and imports its values from + /// Returns a new [AssetEditsCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditActionListDto? fromJson(dynamic value) { - upgradeDto(value, "AssetEditActionListDto"); + static AssetEditsCreateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsCreateDto"); if (value is Map) { final json = value.cast(); - return AssetEditActionListDto( - edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']), + return AssetEditsCreateDto( + edits: AssetEditActionItemDto.listFromJson(json[r'edits']), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditActionListDto.fromJson(row); + final value = AssetEditsCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -65,12 +65,12 @@ class AssetEditActionListDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditActionListDto.fromJson(entry.value); + final value = AssetEditsCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -79,14 +79,14 @@ class AssetEditActionListDto { return map; } - // maps a json object with a list of AssetEditActionListDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditsCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditActionListDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditsCreateDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/asset_edits_dto.dart b/mobile/openapi/lib/model/asset_edits_response_dto.dart similarity index 57% rename from mobile/openapi/lib/model/asset_edits_dto.dart rename to mobile/openapi/lib/model/asset_edits_response_dto.dart index 3bfbce8594..322b4c0a4c 100644 --- a/mobile/openapi/lib/model/asset_edits_dto.dart +++ b/mobile/openapi/lib/model/asset_edits_response_dto.dart @@ -10,21 +10,21 @@ part of openapi.api; -class AssetEditsDto { - /// Returns a new [AssetEditsDto] instance. - AssetEditsDto({ +class AssetEditsResponseDto { + /// Returns a new [AssetEditsResponseDto] instance. + AssetEditsResponseDto({ required this.assetId, this.edits = const [], }); - /// Asset ID to apply edits to + /// Asset ID these edits belong to String assetId; - /// List of edit actions to apply (crop, rotate, or mirror) - List edits; + /// List of edit actions applied to the asset + List edits; @override - bool operator ==(Object other) => identical(this, other) || other is AssetEditsDto && + bool operator ==(Object other) => identical(this, other) || other is AssetEditsResponseDto && other.assetId == assetId && _deepEquality.equals(other.edits, edits); @@ -35,7 +35,7 @@ class AssetEditsDto { (edits.hashCode); @override - String toString() => 'AssetEditsDto[assetId=$assetId, edits=$edits]'; + String toString() => 'AssetEditsResponseDto[assetId=$assetId, edits=$edits]'; Map toJson() { final json = {}; @@ -44,27 +44,27 @@ class AssetEditsDto { return json; } - /// Returns a new [AssetEditsDto] instance and imports its values from + /// Returns a new [AssetEditsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AssetEditsDto? fromJson(dynamic value) { - upgradeDto(value, "AssetEditsDto"); + static AssetEditsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsResponseDto"); if (value is Map) { final json = value.cast(); - return AssetEditsDto( + return AssetEditsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - edits: AssetEditActionListDtoEditsInner.listFromJson(json[r'edits']), + edits: AssetEditActionItemResponseDto.listFromJson(json[r'edits']), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetEditsDto.fromJson(row); + final value = AssetEditsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class AssetEditsDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AssetEditsDto.fromJson(entry.value); + final value = AssetEditsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class AssetEditsDto { return map; } - // maps a json object with a list of AssetEditsDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AssetEditsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AssetEditsDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AssetEditsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index da654f0907..d519e53ce3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -3703,7 +3703,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditsDto" + "$ref": "#/components/schemas/AssetEditsResponseDto" } } }, @@ -3756,7 +3756,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditActionListDto" + "$ref": "#/components/schemas/AssetEditsCreateDto" } } }, @@ -3767,7 +3767,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetEditsDto" + "$ref": "#/components/schemas/AssetEditsResponseDto" } } }, @@ -16082,7 +16082,7 @@ ], "type": "string" }, - "AssetEditActionCrop": { + "AssetEditActionItemDto": { "properties": { "action": { "allOf": [ @@ -16093,7 +16093,18 @@ "description": "Type of edit action to perform" }, "parameters": { - "$ref": "#/components/schemas/CropParameters" + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" } }, "required": [ @@ -16102,30 +16113,48 @@ ], "type": "object" }, - "AssetEditActionListDto": { + "AssetEditActionItemResponseDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" + } + }, + "required": [ + "action", + "id", + "parameters" + ], + "type": "object" + }, + "AssetEditsCreateDto": { "properties": { "edits": { "description": "List of edit actions to apply (crop, rotate, or mirror)", "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/AssetEditActionCrop" - }, - { - "$ref": "#/components/schemas/AssetEditActionRotate" - }, - { - "$ref": "#/components/schemas/AssetEditActionMirror" - } - ], - "discriminator": { - "mapping": { - "crop": "#/components/schemas/AssetEditActionCrop", - "mirror": "#/components/schemas/AssetEditActionMirror", - "rotate": "#/components/schemas/AssetEditActionRotate" - }, - "propertyName": "action" - } + "$ref": "#/components/schemas/AssetEditActionItemDto" }, "minItems": 1, "type": "array" @@ -16136,77 +16165,18 @@ ], "type": "object" }, - "AssetEditActionMirror": { - "properties": { - "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" - }, - "parameters": { - "$ref": "#/components/schemas/MirrorParameters" - } - }, - "required": [ - "action", - "parameters" - ], - "type": "object" - }, - "AssetEditActionRotate": { - "properties": { - "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" - }, - "parameters": { - "$ref": "#/components/schemas/RotateParameters" - } - }, - "required": [ - "action", - "parameters" - ], - "type": "object" - }, - "AssetEditsDto": { + "AssetEditsResponseDto": { "properties": { "assetId": { - "description": "Asset ID to apply edits to", + "description": "Asset ID these edits belong to", "format": "uuid", "type": "string" }, "edits": { - "description": "List of edit actions to apply (crop, rotate, or mirror)", + "description": "List of edit actions applied to the asset", "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/AssetEditActionCrop" - }, - { - "$ref": "#/components/schemas/AssetEditActionRotate" - }, - { - "$ref": "#/components/schemas/AssetEditActionMirror" - } - ], - "discriminator": { - "mapping": { - "crop": "#/components/schemas/AssetEditActionCrop", - "mirror": "#/components/schemas/AssetEditActionMirror", - "rotate": "#/components/schemas/AssetEditActionRotate" - }, - "propertyName": "action" - } + "$ref": "#/components/schemas/AssetEditActionItemResponseDto" }, - "minItems": 1, "type": "array" } }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index abf14d5340..8fda52e9ee 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -959,38 +959,36 @@ export type CropParameters = { /** Top-Left Y coordinate of crop */ y: number; }; -export type AssetEditActionCrop = { - /** Type of edit action to perform */ - action: AssetEditAction; - parameters: CropParameters; -}; export type RotateParameters = { /** Rotation angle in degrees */ angle: number; }; -export type AssetEditActionRotate = { - /** Type of edit action to perform */ - action: AssetEditAction; - parameters: RotateParameters; -}; export type MirrorParameters = { /** Axis to mirror along */ axis: MirrorAxis; }; -export type AssetEditActionMirror = { +export type AssetEditActionItemResponseDto = { /** Type of edit action to perform */ action: AssetEditAction; - parameters: MirrorParameters; + id: string; + /** List of edit actions to apply (crop, rotate, or mirror) */ + parameters: CropParameters | RotateParameters | MirrorParameters; }; -export type AssetEditsDto = { - /** Asset ID to apply edits to */ +export type AssetEditsResponseDto = { + /** Asset ID these edits belong to */ assetId: string; - /** List of edit actions to apply (crop, rotate, or mirror) */ - edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; + /** List of edit actions applied to the asset */ + edits: AssetEditActionItemResponseDto[]; }; -export type AssetEditActionListDto = { +export type AssetEditActionItemDto = { + /** Type of edit action to perform */ + action: AssetEditAction; /** List of edit actions to apply (crop, rotate, or mirror) */ - edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; + parameters: CropParameters | RotateParameters | MirrorParameters; +}; +export type AssetEditsCreateDto = { + /** List of edit actions to apply (crop, rotate, or mirror) */ + edits: AssetEditActionItemDto[]; }; export type AssetMetadataResponseDto = { /** Metadata key */ @@ -4149,7 +4147,7 @@ export function getAssetEdits({ id }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetEditsDto; + data: AssetEditsResponseDto; }>(`/assets/${encodeURIComponent(id)}/edits`, { ...opts })); @@ -4157,17 +4155,17 @@ export function getAssetEdits({ id }: { /** * Apply edits to an existing asset */ -export function editAsset({ id, assetEditActionListDto }: { +export function editAsset({ id, assetEditsCreateDto }: { id: string; - assetEditActionListDto: AssetEditActionListDto; + assetEditsCreateDto: AssetEditsCreateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetEditsDto; + data: AssetEditsResponseDto; }>(`/assets/${encodeURIComponent(id)}/edits`, oazapfts.json({ ...opts, method: "PUT", - body: assetEditActionListDto + body: assetEditsCreateDto }))); } /** diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 197e06d02d..2893a27539 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -369,6 +369,31 @@ describe(AssetController.name, () => { expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); }); + it('should check the action and parameters discriminator', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/edits`) + .send({ + edits: [ + { + action: 'rotate', + parameters: { + x: 0, + y: 0, + width: 100, + height: 100, + }, + }, + ], + }); + + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining([expect.stringContaining('parameters.angle must be one of the following values')]), + ), + ); + }); + it('should require at least one edit', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/assets/${factory.uuid()}/edits`) diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8eb3a5ce44..2024760975 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -20,7 +20,7 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEditActionListDto, AssetEditsDto } from 'src/dtos/editing.dto'; +import { AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { ApiTag, Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -235,7 +235,7 @@ export class AssetController { description: 'Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0'), }) - getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getAssetEdits(auth, id); } @@ -249,8 +249,8 @@ export class AssetController { editAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: AssetEditActionListDto, - ): Promise { + @Body() dto: AssetEditsCreateDto, + ): Promise { return this.service.editAsset(auth, id, dto); } diff --git a/server/src/dtos/editing.dto.ts b/server/src/dtos/editing.dto.ts index 3c4c063b10..fcdfdcad5f 100644 --- a/server/src/dtos/editing.dto.ts +++ b/server/src/dtos/editing.dto.ts @@ -1,7 +1,8 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer'; +import { ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; -import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateUUID } from 'src/validation'; +import { ExtraModel } from 'src/dtos/sync.dto'; +import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; export enum AssetEditAction { Crop = 'crop', @@ -14,6 +15,7 @@ export enum MirrorAxis { Vertical = 'vertical', } +@ExtraModel() export class CropParameters { @IsInt() @Min(0) @@ -36,48 +38,21 @@ export class CropParameters { height!: number; } +@ExtraModel() export class RotateParameters { @IsAxisAlignedRotation() @ApiProperty({ description: 'Rotation angle in degrees' }) angle!: number; } +@ExtraModel() export class MirrorParameters { @IsEnum(MirrorAxis) @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) axis!: MirrorAxis; } -class AssetEditActionBase { - @IsEnum(AssetEditAction) - @ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction', description: 'Type of edit action to perform' }) - action!: AssetEditAction; -} - -export class AssetEditActionCrop extends AssetEditActionBase { - @ValidateNested() - @Type(() => CropParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: CropParameters; -} - -export class AssetEditActionRotate extends AssetEditActionBase { - @ValidateNested() - @Type(() => RotateParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: RotateParameters; -} - -export class AssetEditActionMirror extends AssetEditActionBase { - @ValidateNested() - @Type(() => MirrorParameters) - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - parameters!: MirrorParameters; -} - +export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; export type AssetEditActionItem = | { action: AssetEditAction.Crop; @@ -92,47 +67,48 @@ export type AssetEditActionItem = parameters: MirrorParameters; }; -export type AssetEditActionParameter = { - [AssetEditAction.Crop]: CropParameters; - [AssetEditAction.Rotate]: RotateParameters; - [AssetEditAction.Mirror]: MirrorParameters; +export class AssetEditActionItemDto { + @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) + action!: AssetEditAction; + + @ApiProperty({ + description: 'List of edit actions to apply (crop, rotate, or mirror)', + anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ + $ref: getSchemaPath(type), + })), + }) + @ValidateNested() + @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) + parameters!: AssetEditActionItem['parameters']; +} + +export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { + @ValidateUUID() + id!: string; +} + +export type AssetEditActionParameter = typeof actionParameterMap; +const actionParameterMap = { + [AssetEditAction.Crop]: CropParameters, + [AssetEditAction.Rotate]: RotateParameters, + [AssetEditAction.Mirror]: MirrorParameters, }; -type AssetEditActions = AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror; -const actionToClass: Record> = { - [AssetEditAction.Crop]: AssetEditActionCrop, - [AssetEditAction.Rotate]: AssetEditActionRotate, - [AssetEditAction.Mirror]: AssetEditActionMirror, -} as const; - -const getActionClass = (item: { action: AssetEditAction }): ClassConstructor => - actionToClass[item.action]; - -@ApiExtraModels(AssetEditActionRotate, AssetEditActionMirror, AssetEditActionCrop) -export class AssetEditActionListDto { - /** list of edits */ +export class AssetEditsCreateDto { @ArrayMinSize(1) @IsUniqueEditActions() @ValidateNested({ each: true }) - @Transform(({ value: edits }) => - Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits, - ) - @ApiProperty({ - items: { - anyOf: Object.values(actionToClass).map((type) => ({ $ref: getSchemaPath(type) })), - discriminator: { - propertyName: 'action', - mapping: Object.fromEntries( - Object.entries(actionToClass).map(([action, type]) => [action, getSchemaPath(type)]), - ), - }, - }, - description: 'List of edit actions to apply (crop, rotate, or mirror)', - }) - edits!: AssetEditActionItem[]; + @Type(() => AssetEditActionItemDto) + @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) + edits!: AssetEditActionItemDto[]; } -export class AssetEditsDto extends AssetEditActionListDto { - @ValidateUUID({ description: 'Asset ID to apply edits to' }) +export class AssetEditsResponseDto { + @ValidateUUID({ description: 'Asset ID these edits belong to' }) assetId!: string; + + @ApiProperty({ + description: 'List of edit actions applied to the asset', + }) + edits!: AssetEditActionItemResponseDto[]; } diff --git a/server/src/queries/asset.edit.repository.sql b/server/src/queries/asset.edit.repository.sql index 0cf62882db..9330305973 100644 --- a/server/src/queries/asset.edit.repository.sql +++ b/server/src/queries/asset.edit.repository.sql @@ -9,6 +9,7 @@ rollback -- AssetEditRepository.getAll select + "id", "action", "parameters" from diff --git a/server/src/repositories/asset-edit.repository.ts b/server/src/repositories/asset-edit.repository.ts index 088cb1ccff..23c7c3a0b9 100644 --- a/server/src/repositories/asset-edit.repository.ts +++ b/server/src/repositories/asset-edit.repository.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Kysely } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { AssetEditActionItem, AssetEditActionItemResponseDto } from 'src/dtos/editing.dto'; import { DB } from 'src/schema'; @Injectable() @@ -12,7 +12,7 @@ export class AssetEditRepository { @GenerateSql({ params: [DummyValue.UUID], }) - replaceAll(assetId: string, edits: AssetEditActionItem[]): Promise { + replaceAll(assetId: string, edits: AssetEditActionItem[]): Promise { return this.db.transaction().execute(async (trx) => { await trx.deleteFrom('asset_edit').where('assetId', '=', assetId).execute(); @@ -20,8 +20,8 @@ export class AssetEditRepository { return trx .insertInto('asset_edit') .values(edits.map((edit, i) => ({ assetId, sequence: i, ...edit }))) - .returning(['action', 'parameters']) - .execute() as Promise; + .returning(['id', 'action', 'parameters']) + .execute(); } return []; @@ -31,12 +31,12 @@ export class AssetEditRepository { @GenerateSql({ params: [DummyValue.UUID], }) - getAll(assetId: string): Promise { + getAll(assetId: string): Promise { return this.db .selectFrom('asset_edit') - .select(['action', 'parameters']) + .select(['id', 'action', 'parameters']) .where('assetId', '=', assetId) .orderBy('sequence', 'asc') - .execute() as Promise; + .execute(); } } diff --git a/server/src/schema/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts index 51d3ed0a4a..1ec6bf081c 100644 --- a/server/src/schema/tables/asset-edit.table.ts +++ b/server/src/schema/tables/asset-edit.table.ts @@ -8,7 +8,7 @@ import { Table, Unique, } from '@immich/sql-tools'; -import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; +import { AssetEditAction, AssetEditParameters } from 'src/dtos/editing.dto'; import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; @@ -21,7 +21,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; when: 'pg_trigger_depth() = 0', }) @Unique({ columns: ['assetId', 'sequence'] }) -export class AssetEditTable { +export class AssetEditTable { @PrimaryGeneratedColumn() id!: Generated; @@ -29,10 +29,10 @@ export class AssetEditTable { assetId!: string; @Column() - action!: T; + action!: AssetEditAction; @Column({ type: 'jsonb' }) - parameters!: AssetEditActionParameter[T]; + parameters!: AssetEditParameters; @Column({ type: 'integer' }) sequence!: number; diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index f6098248ed..32225545bb 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -21,7 +21,7 @@ import { mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetEditAction, AssetEditActionCrop, AssetEditActionListDto, AssetEditsDto } from 'src/dtos/editing.dto'; +import { AssetEditAction, AssetEditActionItem, AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { AssetFileType, @@ -543,7 +543,7 @@ export class AssetService extends BaseService { } } - async getAssetEdits(auth: AuthDto, id: string): Promise { + async getAssetEdits(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); const edits = await this.assetEditRepository.getAll(id); return { @@ -552,7 +552,7 @@ export class AssetService extends BaseService { }; } - async editAsset(auth: AuthDto, id: string, dto: AssetEditActionListDto): Promise { + async editAsset(auth: AuthDto, id: string, dto: AssetEditsCreateDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetEditCreate, ids: [id] }); const asset = await this.assetRepository.getForEdit(id); @@ -587,12 +587,13 @@ export class AssetService extends BaseService { throw new BadRequestException('Asset dimensions are not available for editing'); } - const cropIndex = dto.edits.findIndex((e) => e.action === AssetEditAction.Crop); - if (cropIndex > 0) { - throw new BadRequestException('Crop action must be the first edit action'); - } - const crop = cropIndex === -1 ? null : (dto.edits[cropIndex] as AssetEditActionCrop); + const edits = dto.edits as AssetEditActionItem[]; + const crop = edits.find((e) => e.action === AssetEditAction.Crop); if (crop) { + if (edits[0].action !== AssetEditAction.Crop) { + throw new BadRequestException('Crop action must be the first edit action'); + } + // check that crop parameters will not go out of bounds const { width: assetWidth, height: assetHeight } = getDimensions(asset); @@ -606,7 +607,7 @@ export class AssetService extends BaseService { } } - const newEdits = await this.assetEditRepository.replaceAll(id, dto.edits); + const newEdits = await this.assetEditRepository.replaceAll(id, edits); await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } }); // Return the asset and its applied edits diff --git a/server/test/factories/asset-edit.factory.ts b/server/test/factories/asset-edit.factory.ts index e16b0c2e4b..ba15da4b69 100644 --- a/server/test/factories/asset-edit.factory.ts +++ b/server/test/factories/asset-edit.factory.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely'; -import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetFactory } from 'test/factories/asset.factory'; import { build } from 'test/factories/builder.factory'; @@ -33,6 +33,6 @@ export class AssetEditFactory { } build() { - return { ...this.value } as Selectable>; + return { ...this.value } as Omit, 'action' | 'parameters'> & AssetEditActionItem; } } diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index f1b87b50d7..55f219699e 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -6,7 +6,7 @@ import { Stats } from 'node:fs'; import { Writable } from 'node:stream'; import { AssetFace } from 'src/database'; import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto'; -import { AssetEditActionListDto } from 'src/dtos/editing.dto'; +import { AssetEditActionItem, AssetEditsCreateDto } from 'src/dtos/editing.dto'; import { AlbumUserRole, AssetType, @@ -282,8 +282,8 @@ export class MediumTestContext { return { tagsAssets, result }; } - async newEdits(assetId: string, dto: AssetEditActionListDto) { - const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits); + async newEdits(assetId: string, dto: AssetEditsCreateDto) { + const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits as AssetEditActionItem[]); return { edits }; } } diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 477414dafe..2569b29353 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -887,15 +887,16 @@ describe(AssetService.name, () => { await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); const editAction = { action: AssetEditAction.Rotate, parameters: { angle: 90 } } as const; + const editResponse = { ...editAction, id: expect.any(String) }; await expect(sut.editAsset(auth, asset.id, { edits: [editAction] })).resolves.toEqual({ assetId: asset.id, - edits: [editAction], + edits: [editResponse], }); await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual( expect.objectContaining({ isEdited: true }), ); - await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editAction]); + await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editResponse]); }); }); }); diff --git a/web/src/lib/managers/edit/edit-manager.svelte.ts b/web/src/lib/managers/edit/edit-manager.svelte.ts index e9c7b78f13..1366bbea9a 100644 --- a/web/src/lib/managers/edit/edit-manager.svelte.ts +++ b/web/src/lib/managers/edit/edit-manager.svelte.ts @@ -3,12 +3,12 @@ import { transformManager } from '$lib/managers/edit/transform-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; import { waitForWebsocketEvent } from '$lib/stores/websocket'; import { getFormatter } from '$lib/utils/i18n'; -import { editAsset, removeAssetEdits, type AssetEditsDto, type AssetResponseDto } from '@immich/sdk'; +import { editAsset, removeAssetEdits, type AssetEditsCreateDto, type AssetResponseDto } from '@immich/sdk'; import { ConfirmModal, modalManager, toastManager } from '@immich/ui'; import { mdiCropRotate } from '@mdi/js'; import type { Component } from 'svelte'; -export type EditAction = AssetEditsDto['edits'][number]; +export type EditAction = AssetEditsCreateDto['edits'][number]; export type EditActions = EditAction[]; export interface EditToolManager { @@ -84,7 +84,7 @@ export class EditManager { this.selectedTool = this.tools[0]; } - async activateTool(toolType: EditToolType, asset: AssetResponseDto, edits: AssetEditsDto) { + async activateTool(toolType: EditToolType, asset: AssetResponseDto, edits: AssetEditsCreateDto) { this.hasAppliedEdits = false; if (this.selectedTool?.type === toolType) { return; @@ -133,7 +133,7 @@ export class EditManager { ? removeAssetEdits({ id: assetId }) : editAsset({ id: assetId, - assetEditActionListDto: { + assetEditsCreateDto: { edits, }, }));