mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 20:07:41 +09:00 
			
		
		
		
	feat: asset metadata (#20446)
This commit is contained in:
		
							
								
								
									
										10
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -97,16 +97,20 @@ Class | Method | HTTP request | Description | ||||
| *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |  | ||||
| *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload | ||||
| *AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets | ||||
| *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} |  | ||||
| *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |  | ||||
| *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |  | ||||
| *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId | ||||
| *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |  | ||||
| *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata |  | ||||
| *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} |  | ||||
| *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |  | ||||
| *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |  | ||||
| *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |  | ||||
| *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset | ||||
| *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |  | ||||
| *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |  | ||||
| *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata |  | ||||
| *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |  | ||||
| *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |  | ||||
| *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |  | ||||
| @@ -328,6 +332,10 @@ Class | Method | HTTP request | Description | ||||
|  - [AssetMediaResponseDto](doc//AssetMediaResponseDto.md) | ||||
|  - [AssetMediaSize](doc//AssetMediaSize.md) | ||||
|  - [AssetMediaStatus](doc//AssetMediaStatus.md) | ||||
|  - [AssetMetadataKey](doc//AssetMetadataKey.md) | ||||
|  - [AssetMetadataResponseDto](doc//AssetMetadataResponseDto.md) | ||||
|  - [AssetMetadataUpsertDto](doc//AssetMetadataUpsertDto.md) | ||||
|  - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) | ||||
|  - [AssetOrder](doc//AssetOrder.md) | ||||
|  - [AssetResponseDto](doc//AssetResponseDto.md) | ||||
|  - [AssetStackResponseDto](doc//AssetStackResponseDto.md) | ||||
| @@ -485,6 +493,8 @@ Class | Method | HTTP request | Description | ||||
|  - [SyncAssetExifV1](doc//SyncAssetExifV1.md) | ||||
|  - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) | ||||
|  - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) | ||||
|  - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) | ||||
|  - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) | ||||
|  - [SyncAssetV1](doc//SyncAssetV1.md) | ||||
|  - [SyncAuthUserV1](doc//SyncAuthUserV1.md) | ||||
|  - [SyncEntityType](doc//SyncEntityType.md) | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -106,6 +106,10 @@ part 'model/asset_jobs_dto.dart'; | ||||
| part 'model/asset_media_response_dto.dart'; | ||||
| part 'model/asset_media_size.dart'; | ||||
| part 'model/asset_media_status.dart'; | ||||
| part 'model/asset_metadata_key.dart'; | ||||
| part 'model/asset_metadata_response_dto.dart'; | ||||
| part 'model/asset_metadata_upsert_dto.dart'; | ||||
| part 'model/asset_metadata_upsert_item_dto.dart'; | ||||
| part 'model/asset_order.dart'; | ||||
| part 'model/asset_response_dto.dart'; | ||||
| part 'model/asset_stack_response_dto.dart'; | ||||
| @@ -263,6 +267,8 @@ part 'model/sync_asset_delete_v1.dart'; | ||||
| part 'model/sync_asset_exif_v1.dart'; | ||||
| part 'model/sync_asset_face_delete_v1.dart'; | ||||
| part 'model/sync_asset_face_v1.dart'; | ||||
| part 'model/sync_asset_metadata_delete_v1.dart'; | ||||
| part 'model/sync_asset_metadata_v1.dart'; | ||||
| part 'model/sync_asset_v1.dart'; | ||||
| part 'model/sync_auth_user_v1.dart'; | ||||
| part 'model/sync_entity_type.dart'; | ||||
|   | ||||
							
								
								
									
										238
									
								
								mobile/openapi/lib/api/assets_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										238
									
								
								mobile/openapi/lib/api/assets_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -128,6 +128,56 @@ class AssetsApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.update` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataKey] key (required): | ||||
|   Future<Response> deleteAssetMetadataWithHttpInfo(String id, AssetMetadataKey key,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets/{id}/metadata/{key}' | ||||
|       .replaceAll('{id}', id) | ||||
|       .replaceAll('{key}', key.toString()); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       apiPath, | ||||
|       'DELETE', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.update` permission. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataKey] key (required): | ||||
|   Future<void> deleteAssetMetadata(String id, AssetMetadataKey key,) async { | ||||
|     final response = await deleteAssetMetadataWithHttpInfo(id, key,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.delete` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
| @@ -368,6 +418,120 @@ class AssetsApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.read` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   Future<Response> getAssetMetadataWithHttpInfo(String id,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets/{id}/metadata' | ||||
|       .replaceAll('{id}', id); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       apiPath, | ||||
|       'GET', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.read` permission. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   Future<List<AssetMetadataResponseDto>?> getAssetMetadata(String id,) async { | ||||
|     final response = await getAssetMetadataWithHttpInfo(id,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       final responseBody = await _decodeBodyBytes(response); | ||||
|       return (await apiClient.deserializeAsync(responseBody, 'List<AssetMetadataResponseDto>') as List) | ||||
|         .cast<AssetMetadataResponseDto>() | ||||
|         .toList(growable: false); | ||||
| 
 | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.read` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataKey] key (required): | ||||
|   Future<Response> getAssetMetadataByKeyWithHttpInfo(String id, AssetMetadataKey key,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets/{id}/metadata/{key}' | ||||
|       .replaceAll('{id}', id) | ||||
|       .replaceAll('{key}', key.toString()); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       apiPath, | ||||
|       'GET', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.read` permission. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataKey] key (required): | ||||
|   Future<AssetMetadataResponseDto?> getAssetMetadataByKey(String id, AssetMetadataKey key,) async { | ||||
|     final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|     // When a remote server returns no body with a status of 204, we shall not decode it. | ||||
|     // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" | ||||
|     // FormatException when trying to decode an empty string. | ||||
|     if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { | ||||
|       return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMetadataResponseDto',) as AssetMetadataResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.statistics` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
| @@ -795,6 +959,66 @@ class AssetsApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.update` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): | ||||
|   Future<Response> updateAssetMetadataWithHttpInfo(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets/{id}/metadata' | ||||
|       .replaceAll('{id}', id); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = assetMetadataUpsertDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       apiPath, | ||||
|       'PUT', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.update` permission. | ||||
|   /// | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AssetMetadataUpsertDto] assetMetadataUpsertDto (required): | ||||
|   Future<List<AssetMetadataResponseDto>?> updateAssetMetadata(String id, AssetMetadataUpsertDto assetMetadataUpsertDto,) async { | ||||
|     final response = await updateAssetMetadataWithHttpInfo(id, assetMetadataUpsertDto,); | ||||
|     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<AssetMetadataResponseDto>') as List) | ||||
|         .cast<AssetMetadataResponseDto>() | ||||
|         .toList(growable: false); | ||||
| 
 | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// This endpoint requires the `asset.update` permission. | ||||
|   /// | ||||
|   /// Note: This method returns the HTTP [Response]. | ||||
| @@ -855,6 +1079,8 @@ class AssetsApi { | ||||
|   /// | ||||
|   /// * [DateTime] fileModifiedAt (required): | ||||
|   /// | ||||
|   /// * [List<AssetMetadataUpsertItemDto>] metadata (required): | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   /// | ||||
|   /// * [String] slug: | ||||
| @@ -873,7 +1099,7 @@ class AssetsApi { | ||||
|   /// * [MultipartFile] sidecarData: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|   Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/assets'; | ||||
| 
 | ||||
| @@ -936,6 +1162,10 @@ class AssetsApi { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); | ||||
|     } | ||||
|     if (metadata != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'metadata'] = parameterToString(metadata); | ||||
|     } | ||||
|     if (sidecarData != null) { | ||||
|       hasFields = true; | ||||
|       mp.fields[r'sidecarData'] = sidecarData.field; | ||||
| @@ -974,6 +1204,8 @@ class AssetsApi { | ||||
|   /// | ||||
|   /// * [DateTime] fileModifiedAt (required): | ||||
|   /// | ||||
|   /// * [List<AssetMetadataUpsertItemDto>] metadata (required): | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   /// | ||||
|   /// * [String] slug: | ||||
| @@ -992,8 +1224,8 @@ class AssetsApi { | ||||
|   /// * [MultipartFile] sidecarData: | ||||
|   /// | ||||
|   /// * [AssetVisibility] visibility: | ||||
|   Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|     final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt,  key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); | ||||
|   Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List<AssetMetadataUpsertItemDto> metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { | ||||
|     final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, metadata,  key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -266,6 +266,14 @@ class ApiClient { | ||||
|           return AssetMediaSizeTypeTransformer().decode(value); | ||||
|         case 'AssetMediaStatus': | ||||
|           return AssetMediaStatusTypeTransformer().decode(value); | ||||
|         case 'AssetMetadataKey': | ||||
|           return AssetMetadataKeyTypeTransformer().decode(value); | ||||
|         case 'AssetMetadataResponseDto': | ||||
|           return AssetMetadataResponseDto.fromJson(value); | ||||
|         case 'AssetMetadataUpsertDto': | ||||
|           return AssetMetadataUpsertDto.fromJson(value); | ||||
|         case 'AssetMetadataUpsertItemDto': | ||||
|           return AssetMetadataUpsertItemDto.fromJson(value); | ||||
|         case 'AssetOrder': | ||||
|           return AssetOrderTypeTransformer().decode(value); | ||||
|         case 'AssetResponseDto': | ||||
| @@ -580,6 +588,10 @@ class ApiClient { | ||||
|           return SyncAssetFaceDeleteV1.fromJson(value); | ||||
|         case 'SyncAssetFaceV1': | ||||
|           return SyncAssetFaceV1.fromJson(value); | ||||
|         case 'SyncAssetMetadataDeleteV1': | ||||
|           return SyncAssetMetadataDeleteV1.fromJson(value); | ||||
|         case 'SyncAssetMetadataV1': | ||||
|           return SyncAssetMetadataV1.fromJson(value); | ||||
|         case 'SyncAssetV1': | ||||
|           return SyncAssetV1.fromJson(value); | ||||
|         case 'SyncAuthUserV1': | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/api_helper.dart
									
									
									
										generated
									
									
									
								
							| @@ -67,6 +67,9 @@ String parameterToString(dynamic value) { | ||||
|   if (value is AssetMediaStatus) { | ||||
|     return AssetMediaStatusTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   if (value is AssetMetadataKey) { | ||||
|     return AssetMetadataKeyTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   if (value is AssetOrder) { | ||||
|     return AssetOrderTypeTransformer().encode(value).toString(); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										82
									
								
								mobile/openapi/lib/model/asset_metadata_key.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								mobile/openapi/lib/model/asset_metadata_key.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| // | ||||
| // 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 AssetMetadataKey { | ||||
|   /// Instantiate a new enum with the provided [value]. | ||||
|   const AssetMetadataKey._(this.value); | ||||
| 
 | ||||
|   /// The underlying value of this enum member. | ||||
|   final String value; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => value; | ||||
| 
 | ||||
|   String toJson() => value; | ||||
| 
 | ||||
|   static const mobileApp = AssetMetadataKey._(r'mobile-app'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][AssetMetadataKey]. | ||||
|   static const values = <AssetMetadataKey>[ | ||||
|     mobileApp, | ||||
|   ]; | ||||
| 
 | ||||
|   static AssetMetadataKey? fromJson(dynamic value) => AssetMetadataKeyTypeTransformer().decode(value); | ||||
| 
 | ||||
|   static List<AssetMetadataKey> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetMetadataKey>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetMetadataKey.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /// Transformation class that can [encode] an instance of [AssetMetadataKey] to String, | ||||
| /// and [decode] dynamic data back to [AssetMetadataKey]. | ||||
| class AssetMetadataKeyTypeTransformer { | ||||
|   factory AssetMetadataKeyTypeTransformer() => _instance ??= const AssetMetadataKeyTypeTransformer._(); | ||||
| 
 | ||||
|   const AssetMetadataKeyTypeTransformer._(); | ||||
| 
 | ||||
|   String encode(AssetMetadataKey data) => data.value; | ||||
| 
 | ||||
|   /// Decodes a [dynamic value][data] to a AssetMetadataKey. | ||||
|   /// | ||||
|   /// 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. | ||||
|   AssetMetadataKey? decode(dynamic data, {bool allowNull = true}) { | ||||
|     if (data != null) { | ||||
|       switch (data) { | ||||
|         case r'mobile-app': return AssetMetadataKey.mobileApp; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|           } | ||||
|       } | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Singleton [AssetMetadataKeyTypeTransformer] instance. | ||||
|   static AssetMetadataKeyTypeTransformer? _instance; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										115
									
								
								mobile/openapi/lib/model/asset_metadata_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/openapi/lib/model/asset_metadata_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| // | ||||
| // 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 AssetMetadataResponseDto { | ||||
|   /// Returns a new [AssetMetadataResponseDto] instance. | ||||
|   AssetMetadataResponseDto({ | ||||
|     required this.key, | ||||
|     required this.updatedAt, | ||||
|     required this.value, | ||||
|   }); | ||||
| 
 | ||||
|   AssetMetadataKey key; | ||||
| 
 | ||||
|   DateTime updatedAt; | ||||
| 
 | ||||
|   Object value; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && | ||||
|     other.key == key && | ||||
|     other.updatedAt == updatedAt && | ||||
|     other.value == value; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (key.hashCode) + | ||||
|     (updatedAt.hashCode) + | ||||
|     (value.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AssetMetadataResponseDto[key=$key, updatedAt=$updatedAt, value=$value]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'key'] = this.key; | ||||
|       json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); | ||||
|       json[r'value'] = this.value; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AssetMetadataResponseDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AssetMetadataResponseDto? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "AssetMetadataResponseDto"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AssetMetadataResponseDto( | ||||
|         key: AssetMetadataKey.fromJson(json[r'key'])!, | ||||
|         updatedAt: mapDateTime(json, r'updatedAt', r'')!, | ||||
|         value: mapValueOfType<Object>(json, r'value')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AssetMetadataResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetMetadataResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetMetadataResponseDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AssetMetadataResponseDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AssetMetadataResponseDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AssetMetadataResponseDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AssetMetadataResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<AssetMetadataResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AssetMetadataResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AssetMetadataResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'key', | ||||
|     'updatedAt', | ||||
|     'value', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										99
									
								
								mobile/openapi/lib/model/asset_metadata_upsert_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								mobile/openapi/lib/model/asset_metadata_upsert_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| // | ||||
| // 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 AssetMetadataUpsertDto { | ||||
|   /// Returns a new [AssetMetadataUpsertDto] instance. | ||||
|   AssetMetadataUpsertDto({ | ||||
|     this.items = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<AssetMetadataUpsertItemDto> items; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertDto && | ||||
|     _deepEquality.equals(other.items, items); | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (items.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AssetMetadataUpsertDto[items=$items]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'items'] = this.items; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AssetMetadataUpsertDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AssetMetadataUpsertDto? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "AssetMetadataUpsertDto"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AssetMetadataUpsertDto( | ||||
|         items: AssetMetadataUpsertItemDto.listFromJson(json[r'items']), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AssetMetadataUpsertDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetMetadataUpsertDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetMetadataUpsertDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AssetMetadataUpsertDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AssetMetadataUpsertDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AssetMetadataUpsertDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AssetMetadataUpsertDto-objects as value to a dart map | ||||
|   static Map<String, List<AssetMetadataUpsertDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AssetMetadataUpsertDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AssetMetadataUpsertDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'items', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										107
									
								
								mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| // | ||||
| // 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 AssetMetadataUpsertItemDto { | ||||
|   /// Returns a new [AssetMetadataUpsertItemDto] instance. | ||||
|   AssetMetadataUpsertItemDto({ | ||||
|     required this.key, | ||||
|     required this.value, | ||||
|   }); | ||||
| 
 | ||||
|   AssetMetadataKey key; | ||||
| 
 | ||||
|   Object value; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && | ||||
|     other.key == key && | ||||
|     other.value == value; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (key.hashCode) + | ||||
|     (value.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AssetMetadataUpsertItemDto[key=$key, value=$value]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'key'] = this.key; | ||||
|       json[r'value'] = this.value; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AssetMetadataUpsertItemDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AssetMetadataUpsertItemDto? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "AssetMetadataUpsertItemDto"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AssetMetadataUpsertItemDto( | ||||
|         key: AssetMetadataKey.fromJson(json[r'key'])!, | ||||
|         value: mapValueOfType<Object>(json, r'value')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AssetMetadataUpsertItemDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetMetadataUpsertItemDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetMetadataUpsertItemDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AssetMetadataUpsertItemDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AssetMetadataUpsertItemDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AssetMetadataUpsertItemDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AssetMetadataUpsertItemDto-objects as value to a dart map | ||||
|   static Map<String, List<AssetMetadataUpsertItemDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AssetMetadataUpsertItemDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AssetMetadataUpsertItemDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'key', | ||||
|     'value', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										107
									
								
								mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| // | ||||
| // 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 SyncAssetMetadataDeleteV1 { | ||||
|   /// Returns a new [SyncAssetMetadataDeleteV1] instance. | ||||
|   SyncAssetMetadataDeleteV1({ | ||||
|     required this.assetId, | ||||
|     required this.key, | ||||
|   }); | ||||
| 
 | ||||
|   String assetId; | ||||
| 
 | ||||
|   AssetMetadataKey key; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataDeleteV1 && | ||||
|     other.assetId == assetId && | ||||
|     other.key == key; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (assetId.hashCode) + | ||||
|     (key.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SyncAssetMetadataDeleteV1[assetId=$assetId, key=$key]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'assetId'] = this.assetId; | ||||
|       json[r'key'] = this.key; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SyncAssetMetadataDeleteV1] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SyncAssetMetadataDeleteV1? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "SyncAssetMetadataDeleteV1"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return SyncAssetMetadataDeleteV1( | ||||
|         assetId: mapValueOfType<String>(json, r'assetId')!, | ||||
|         key: AssetMetadataKey.fromJson(json[r'key'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SyncAssetMetadataDeleteV1> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SyncAssetMetadataDeleteV1>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SyncAssetMetadataDeleteV1.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SyncAssetMetadataDeleteV1> mapFromJson(dynamic json) { | ||||
|     final map = <String, SyncAssetMetadataDeleteV1>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SyncAssetMetadataDeleteV1.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SyncAssetMetadataDeleteV1-objects as value to a dart map | ||||
|   static Map<String, List<SyncAssetMetadataDeleteV1>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SyncAssetMetadataDeleteV1>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = SyncAssetMetadataDeleteV1.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'assetId', | ||||
|     'key', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										115
									
								
								mobile/openapi/lib/model/sync_asset_metadata_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/openapi/lib/model/sync_asset_metadata_v1.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| // | ||||
| // 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 SyncAssetMetadataV1 { | ||||
|   /// Returns a new [SyncAssetMetadataV1] instance. | ||||
|   SyncAssetMetadataV1({ | ||||
|     required this.assetId, | ||||
|     required this.key, | ||||
|     required this.value, | ||||
|   }); | ||||
| 
 | ||||
|   String assetId; | ||||
| 
 | ||||
|   AssetMetadataKey key; | ||||
| 
 | ||||
|   Object value; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && | ||||
|     other.assetId == assetId && | ||||
|     other.key == key && | ||||
|     other.value == value; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (assetId.hashCode) + | ||||
|     (key.hashCode) + | ||||
|     (value.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SyncAssetMetadataV1[assetId=$assetId, key=$key, value=$value]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'assetId'] = this.assetId; | ||||
|       json[r'key'] = this.key; | ||||
|       json[r'value'] = this.value; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SyncAssetMetadataV1] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SyncAssetMetadataV1? fromJson(dynamic value) { | ||||
|     upgradeDto(value, "SyncAssetMetadataV1"); | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return SyncAssetMetadataV1( | ||||
|         assetId: mapValueOfType<String>(json, r'assetId')!, | ||||
|         key: AssetMetadataKey.fromJson(json[r'key'])!, | ||||
|         value: mapValueOfType<Object>(json, r'value')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SyncAssetMetadataV1> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SyncAssetMetadataV1>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SyncAssetMetadataV1.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SyncAssetMetadataV1> mapFromJson(dynamic json) { | ||||
|     final map = <String, SyncAssetMetadataV1>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SyncAssetMetadataV1.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SyncAssetMetadataV1-objects as value to a dart map | ||||
|   static Map<String, List<SyncAssetMetadataV1>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SyncAssetMetadataV1>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = SyncAssetMetadataV1.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'assetId', | ||||
|     'key', | ||||
|     'value', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										6
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/lib/model/sync_entity_type.dart
									
									
									
										generated
									
									
									
								
							| @@ -29,6 +29,8 @@ class SyncEntityType { | ||||
|   static const assetV1 = SyncEntityType._(r'AssetV1'); | ||||
|   static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); | ||||
|   static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); | ||||
|   static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); | ||||
|   static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); | ||||
|   static const partnerV1 = SyncEntityType._(r'PartnerV1'); | ||||
|   static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); | ||||
|   static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); | ||||
| @@ -76,6 +78,8 @@ class SyncEntityType { | ||||
|     assetV1, | ||||
|     assetDeleteV1, | ||||
|     assetExifV1, | ||||
|     assetMetadataV1, | ||||
|     assetMetadataDeleteV1, | ||||
|     partnerV1, | ||||
|     partnerDeleteV1, | ||||
|     partnerAssetV1, | ||||
| @@ -158,6 +162,8 @@ class SyncEntityTypeTypeTransformer { | ||||
|         case r'AssetV1': return SyncEntityType.assetV1; | ||||
|         case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; | ||||
|         case r'AssetExifV1': return SyncEntityType.assetExifV1; | ||||
|         case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; | ||||
|         case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; | ||||
|         case r'PartnerV1': return SyncEntityType.partnerV1; | ||||
|         case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; | ||||
|         case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/lib/model/sync_request_type.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/lib/model/sync_request_type.dart
									
									
									
										generated
									
									
									
								
							| @@ -30,6 +30,7 @@ class SyncRequestType { | ||||
|   static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); | ||||
|   static const assetsV1 = SyncRequestType._(r'AssetsV1'); | ||||
|   static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); | ||||
|   static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); | ||||
|   static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); | ||||
|   static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); | ||||
|   static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); | ||||
| @@ -52,6 +53,7 @@ class SyncRequestType { | ||||
|     albumAssetExifsV1, | ||||
|     assetsV1, | ||||
|     assetExifsV1, | ||||
|     assetMetadataV1, | ||||
|     authUsersV1, | ||||
|     memoriesV1, | ||||
|     memoryToAssetsV1, | ||||
| @@ -109,6 +111,7 @@ class SyncRequestTypeTypeTransformer { | ||||
|         case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; | ||||
|         case r'AssetsV1': return SyncRequestType.assetsV1; | ||||
|         case r'AssetExifsV1': return SyncRequestType.assetExifsV1; | ||||
|         case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; | ||||
|         case r'AuthUsersV1': return SyncRequestType.authUsersV1; | ||||
|         case r'MemoriesV1': return SyncRequestType.memoriesV1; | ||||
|         case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; | ||||
|   | ||||
| @@ -2245,6 +2245,203 @@ | ||||
|         "description": "This endpoint requires the `asset.update` permission." | ||||
|       } | ||||
|     }, | ||||
|     "/assets/{id}/metadata": { | ||||
|       "get": { | ||||
|         "operationId": "getAssetMetadata", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "id", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/AssetMetadataResponseDto" | ||||
|                   }, | ||||
|                   "type": "array" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Assets" | ||||
|         ], | ||||
|         "x-immich-permission": "asset.read", | ||||
|         "description": "This endpoint requires the `asset.read` permission." | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateAssetMetadata", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "id", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "requestBody": { | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/AssetMetadataUpsertDto" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "required": true | ||||
|         }, | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/AssetMetadataResponseDto" | ||||
|                   }, | ||||
|                   "type": "array" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Assets" | ||||
|         ], | ||||
|         "x-immich-permission": "asset.update", | ||||
|         "description": "This endpoint requires the `asset.update` permission." | ||||
|       } | ||||
|     }, | ||||
|     "/assets/{id}/metadata/{key}": { | ||||
|       "delete": { | ||||
|         "operationId": "deleteAssetMetadata", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "id", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "key", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Assets" | ||||
|         ], | ||||
|         "x-immich-permission": "asset.update", | ||||
|         "description": "This endpoint requires the `asset.update` permission." | ||||
|       }, | ||||
|       "get": { | ||||
|         "operationId": "getAssetMetadataByKey", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "id", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "key", | ||||
|             "required": true, | ||||
|             "in": "path", | ||||
|             "schema": { | ||||
|               "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/AssetMetadataResponseDto" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Assets" | ||||
|         ], | ||||
|         "x-immich-permission": "asset.read", | ||||
|         "description": "This endpoint requires the `asset.read` permission." | ||||
|       } | ||||
|     }, | ||||
|     "/assets/{id}/original": { | ||||
|       "get": { | ||||
|         "operationId": "downloadAsset", | ||||
| @@ -10615,6 +10812,12 @@ | ||||
|             "format": "uuid", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "metadata": { | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "sidecarData": { | ||||
|             "format": "binary", | ||||
|             "type": "string" | ||||
| @@ -10632,7 +10835,8 @@ | ||||
|           "deviceAssetId", | ||||
|           "deviceId", | ||||
|           "fileCreatedAt", | ||||
|           "fileModifiedAt" | ||||
|           "fileModifiedAt", | ||||
|           "metadata" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
| @@ -10707,6 +10911,69 @@ | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "AssetMetadataKey": { | ||||
|         "enum": [ | ||||
|           "mobile-app" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|       "AssetMetadataResponseDto": { | ||||
|         "properties": { | ||||
|           "key": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "updatedAt": { | ||||
|             "format": "date-time", | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "value": { | ||||
|             "type": "object" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "key", | ||||
|           "updatedAt", | ||||
|           "value" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AssetMetadataUpsertDto": { | ||||
|         "properties": { | ||||
|           "items": { | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "items" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AssetMetadataUpsertItemDto": { | ||||
|         "properties": { | ||||
|           "key": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "value": { | ||||
|             "type": "object" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "key", | ||||
|           "value" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AssetOrder": { | ||||
|         "enum": [ | ||||
|           "asc", | ||||
| @@ -14944,6 +15211,48 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SyncAssetMetadataDeleteV1": { | ||||
|         "properties": { | ||||
|           "assetId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "key": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetId", | ||||
|           "key" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SyncAssetMetadataV1": { | ||||
|         "properties": { | ||||
|           "assetId": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "key": { | ||||
|             "allOf": [ | ||||
|               { | ||||
|                 "$ref": "#/components/schemas/AssetMetadataKey" | ||||
|               } | ||||
|             ] | ||||
|           }, | ||||
|           "value": { | ||||
|             "type": "object" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetId", | ||||
|           "key", | ||||
|           "value" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SyncAssetV1": { | ||||
|         "properties": { | ||||
|           "checksum": { | ||||
| @@ -15114,6 +15423,8 @@ | ||||
|           "AssetV1", | ||||
|           "AssetDeleteV1", | ||||
|           "AssetExifV1", | ||||
|           "AssetMetadataV1", | ||||
|           "AssetMetadataDeleteV1", | ||||
|           "PartnerV1", | ||||
|           "PartnerDeleteV1", | ||||
|           "PartnerAssetV1", | ||||
| @@ -15373,6 +15684,7 @@ | ||||
|           "AlbumAssetExifsV1", | ||||
|           "AssetsV1", | ||||
|           "AssetExifsV1", | ||||
|           "AssetMetadataV1", | ||||
|           "AuthUsersV1", | ||||
|           "MemoriesV1", | ||||
|           "MemoryToAssetsV1", | ||||
|   | ||||
| @@ -447,6 +447,10 @@ export type AssetBulkDeleteDto = { | ||||
|     force?: boolean; | ||||
|     ids: string[]; | ||||
| }; | ||||
| export type AssetMetadataUpsertItemDto = { | ||||
|     key: AssetMetadataKey; | ||||
|     value: object; | ||||
| }; | ||||
| export type AssetMediaCreateDto = { | ||||
|     assetData: Blob; | ||||
|     deviceAssetId: string; | ||||
| @@ -457,6 +461,7 @@ export type AssetMediaCreateDto = { | ||||
|     filename?: string; | ||||
|     isFavorite?: boolean; | ||||
|     livePhotoVideoId?: string; | ||||
|     metadata: AssetMetadataUpsertItemDto[]; | ||||
|     sidecarData?: Blob; | ||||
|     visibility?: AssetVisibility; | ||||
| }; | ||||
| @@ -516,6 +521,14 @@ export type UpdateAssetDto = { | ||||
|     rating?: number; | ||||
|     visibility?: AssetVisibility; | ||||
| }; | ||||
| export type AssetMetadataResponseDto = { | ||||
|     key: AssetMetadataKey; | ||||
|     updatedAt: string; | ||||
|     value: object; | ||||
| }; | ||||
| export type AssetMetadataUpsertDto = { | ||||
|     items: AssetMetadataUpsertItemDto[]; | ||||
| }; | ||||
| export type AssetMediaReplaceDto = { | ||||
|     assetData: Blob; | ||||
|     deviceAssetId: string; | ||||
| @@ -2273,6 +2286,61 @@ export function updateAsset({ id, updateAssetDto }: { | ||||
|         body: updateAssetDto | ||||
|     }))); | ||||
| } | ||||
| /** | ||||
|  * This endpoint requires the `asset.read` permission. | ||||
|  */ | ||||
| export function getAssetMetadata({ id }: { | ||||
|     id: string; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchJson<{ | ||||
|         status: 200; | ||||
|         data: AssetMetadataResponseDto[]; | ||||
|     }>(`/assets/${encodeURIComponent(id)}/metadata`, { | ||||
|         ...opts | ||||
|     })); | ||||
| } | ||||
| /** | ||||
|  * This endpoint requires the `asset.update` permission. | ||||
|  */ | ||||
| export function updateAssetMetadata({ id, assetMetadataUpsertDto }: { | ||||
|     id: string; | ||||
|     assetMetadataUpsertDto: AssetMetadataUpsertDto; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchJson<{ | ||||
|         status: 200; | ||||
|         data: AssetMetadataResponseDto[]; | ||||
|     }>(`/assets/${encodeURIComponent(id)}/metadata`, oazapfts.json({ | ||||
|         ...opts, | ||||
|         method: "PUT", | ||||
|         body: assetMetadataUpsertDto | ||||
|     }))); | ||||
| } | ||||
| /** | ||||
|  * This endpoint requires the `asset.update` permission. | ||||
|  */ | ||||
| export function deleteAssetMetadata({ id, key }: { | ||||
|     id: string; | ||||
|     key: AssetMetadataKey; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/metadata/${encodeURIComponent(key)}`, { | ||||
|         ...opts, | ||||
|         method: "DELETE" | ||||
|     })); | ||||
| } | ||||
| /** | ||||
|  * This endpoint requires the `asset.read` permission. | ||||
|  */ | ||||
| export function getAssetMetadataByKey({ id, key }: { | ||||
|     id: string; | ||||
|     key: AssetMetadataKey; | ||||
| }, opts?: Oazapfts.RequestOpts) { | ||||
|     return oazapfts.ok(oazapfts.fetchJson<{ | ||||
|         status: 200; | ||||
|         data: AssetMetadataResponseDto; | ||||
|     }>(`/assets/${encodeURIComponent(id)}/metadata/${encodeURIComponent(key)}`, { | ||||
|         ...opts | ||||
|     })); | ||||
| } | ||||
| /** | ||||
|  * This endpoint requires the `asset.download` permission. | ||||
|  */ | ||||
| @@ -4725,6 +4793,9 @@ export enum Permission { | ||||
|     AdminUserDelete = "adminUser.delete", | ||||
|     AdminAuthUnlinkAll = "adminAuth.unlinkAll" | ||||
| } | ||||
| export enum AssetMetadataKey { | ||||
|     MobileApp = "mobile-app" | ||||
| } | ||||
| export enum AssetMediaStatus { | ||||
|     Created = "created", | ||||
|     Replaced = "replaced", | ||||
| @@ -4811,6 +4882,8 @@ export enum SyncEntityType { | ||||
|     AssetV1 = "AssetV1", | ||||
|     AssetDeleteV1 = "AssetDeleteV1", | ||||
|     AssetExifV1 = "AssetExifV1", | ||||
|     AssetMetadataV1 = "AssetMetadataV1", | ||||
|     AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", | ||||
|     PartnerV1 = "PartnerV1", | ||||
|     PartnerDeleteV1 = "PartnerDeleteV1", | ||||
|     PartnerAssetV1 = "PartnerAssetV1", | ||||
| @@ -4858,6 +4931,7 @@ export enum SyncRequestType { | ||||
|     AlbumAssetExifsV1 = "AlbumAssetExifsV1", | ||||
|     AssetsV1 = "AssetsV1", | ||||
|     AssetExifsV1 = "AssetExifsV1", | ||||
|     AssetMetadataV1 = "AssetMetadataV1", | ||||
|     AuthUsersV1 = "AuthUsersV1", | ||||
|     MemoriesV1 = "MemoriesV1", | ||||
|     MemoryToAssetsV1 = "MemoryToAssetsV1", | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { AssetController } from 'src/controllers/asset.controller'; | ||||
| import { AssetMetadataKey } from 'src/enum'; | ||||
| import { AssetService } from 'src/services/asset.service'; | ||||
| import request from 'supertest'; | ||||
| import { factory } from 'test/small.factory'; | ||||
| @@ -6,14 +7,16 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils' | ||||
|  | ||||
| describe(AssetController.name, () => { | ||||
|   let ctx: ControllerContext; | ||||
|   const service = mockBaseService(AssetService); | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]); | ||||
|     ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: service }]); | ||||
|     return () => ctx.close(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     ctx.reset(); | ||||
|     service.resetAllMocks(); | ||||
|   }); | ||||
|  | ||||
|   describe('PUT /assets', () => { | ||||
| @@ -115,4 +118,120 @@ describe(AssetController.name, () => { | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /assets/:id/metadata', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('PUT /assets/:id/metadata', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({ items: [] }); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should require a valid id', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); | ||||
|     }); | ||||
|  | ||||
|     it('should require items to be an array', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({}); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest(['items must be an array'])); | ||||
|     }); | ||||
|  | ||||
|     it('should require each item to have a valid key', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .put(`/assets/${factory.uuid()}/metadata`) | ||||
|         .send({ items: [{ key: 'someKey' }] }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual( | ||||
|         factory.responses.badRequest( | ||||
|           expect.arrayContaining([expect.stringContaining('items.0.key must be one of the following values')]), | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should require each item to have a value', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()) | ||||
|         .put(`/assets/${factory.uuid()}/metadata`) | ||||
|         .send({ items: [{ key: 'mobile-app', value: null }] }); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual( | ||||
|         factory.responses.badRequest(expect.arrayContaining([expect.stringContaining('value must be an object')])), | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     describe(AssetMetadataKey.MobileApp, () => { | ||||
|       it('should accept valid data and pass to service correctly', async () => { | ||||
|         const assetId = factory.uuid(); | ||||
|         const { status } = await request(ctx.getHttpServer()) | ||||
|           .put(`/assets/${assetId}/metadata`) | ||||
|           .send({ items: [{ key: 'mobile-app', value: { iCloudId: '123' } }] }); | ||||
|         expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { | ||||
|           items: [{ key: 'mobile-app', value: { iCloudId: '123' } }], | ||||
|         }); | ||||
|         expect(status).toBe(200); | ||||
|       }); | ||||
|  | ||||
|       it('should work without iCloudId', async () => { | ||||
|         const assetId = factory.uuid(); | ||||
|         const { status } = await request(ctx.getHttpServer()) | ||||
|           .put(`/assets/${assetId}/metadata`) | ||||
|           .send({ items: [{ key: 'mobile-app', value: {} }] }); | ||||
|         expect(service.upsertMetadata).toHaveBeenCalledWith(undefined, assetId, { | ||||
|           items: [{ key: 'mobile-app', value: {} }], | ||||
|         }); | ||||
|         expect(status).toBe(200); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('GET /assets/:id/metadata/:key', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/mobile-app`); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should require a valid id', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); | ||||
|     }); | ||||
|  | ||||
|     it('should require a valid key', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/invalid`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual( | ||||
|         factory.responses.badRequest( | ||||
|           expect.arrayContaining([expect.stringContaining('key must be one of the following value')]), | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('DELETE /assets/:id/metadata/:key', () => { | ||||
|     it('should be an authenticated route', async () => { | ||||
|       await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`); | ||||
|       expect(ctx.authenticate).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should require a valid id', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); | ||||
|     }); | ||||
|  | ||||
|     it('should require a valid key', async () => { | ||||
|       const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/invalid`); | ||||
|       expect(status).toBe(400); | ||||
|       expect(body).toEqual( | ||||
|         factory.responses.badRequest([expect.stringContaining('key must be one of the following values')]), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -6,6 +6,9 @@ import { | ||||
|   AssetBulkDeleteDto, | ||||
|   AssetBulkUpdateDto, | ||||
|   AssetJobsDto, | ||||
|   AssetMetadataResponseDto, | ||||
|   AssetMetadataRouteParams, | ||||
|   AssetMetadataUpsertDto, | ||||
|   AssetStatsDto, | ||||
|   AssetStatsResponseDto, | ||||
|   DeviceIdDto, | ||||
| @@ -85,4 +88,36 @@ export class AssetController { | ||||
|   ): Promise<AssetResponseDto> { | ||||
|     return this.service.update(auth, id, dto); | ||||
|   } | ||||
|  | ||||
|   @Get(':id/metadata') | ||||
|   @Authenticated({ permission: Permission.AssetRead }) | ||||
|   getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetMetadataResponseDto[]> { | ||||
|     return this.service.getMetadata(auth, id); | ||||
|   } | ||||
|  | ||||
|   @Put(':id/metadata') | ||||
|   @Authenticated({ permission: Permission.AssetUpdate }) | ||||
|   updateAssetMetadata( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|     @Body() dto: AssetMetadataUpsertDto, | ||||
|   ): Promise<AssetMetadataResponseDto[]> { | ||||
|     return this.service.upsertMetadata(auth, id, dto); | ||||
|   } | ||||
|  | ||||
|   @Get(':id/metadata/:key') | ||||
|   @Authenticated({ permission: Permission.AssetRead }) | ||||
|   getAssetMetadataByKey( | ||||
|     @Auth() auth: AuthDto, | ||||
|     @Param() { id, key }: AssetMetadataRouteParams, | ||||
|   ): Promise<AssetMetadataResponseDto> { | ||||
|     return this.service.getMetadataByKey(auth, id, key); | ||||
|   } | ||||
|  | ||||
|   @Delete(':id/metadata/:key') | ||||
|   @Authenticated({ permission: Permission.AssetUpdate }) | ||||
|   @HttpCode(HttpStatus.NO_CONTENT) | ||||
|   deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> { | ||||
|     return this.service.deleteMetadataByKey(auth, id, key); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | ||||
| import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; | ||||
| import { AssetVisibility } from 'src/enum'; | ||||
| import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| @@ -64,6 +65,12 @@ export class AssetMediaCreateDto extends AssetMediaBase { | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   livePhotoVideoId?: string; | ||||
|  | ||||
|   @Optional() | ||||
|   @IsArray() | ||||
|   @ValidateNested({ each: true }) | ||||
|   @Type(() => AssetMetadataUpsertItemDto) | ||||
|   metadata!: AssetMetadataUpsertItemDto[]; | ||||
|  | ||||
|   @ApiProperty({ type: 'string', format: 'binary', required: false }) | ||||
|   [UploadFieldName.SIDECAR_DATA]?: any; | ||||
| } | ||||
|   | ||||
| @@ -1,21 +1,25 @@ | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Type } from 'class-transformer'; | ||||
| import { | ||||
|   IsArray, | ||||
|   IsDateString, | ||||
|   IsInt, | ||||
|   IsLatitude, | ||||
|   IsLongitude, | ||||
|   IsNotEmpty, | ||||
|   IsObject, | ||||
|   IsPositive, | ||||
|   IsString, | ||||
|   IsTimeZone, | ||||
|   Max, | ||||
|   Min, | ||||
|   ValidateIf, | ||||
|   ValidateNested, | ||||
| } from 'class-validator'; | ||||
| import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; | ||||
| import { AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { AssetMetadataKey, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { AssetStats } from 'src/repositories/asset.repository'; | ||||
| import { AssetMetadata, AssetMetadataItem } from 'src/types'; | ||||
| import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; | ||||
|  | ||||
| export class DeviceIdDto { | ||||
| @@ -135,6 +139,53 @@ export class AssetStatsResponseDto { | ||||
|   total!: number; | ||||
| } | ||||
|  | ||||
| export class AssetMetadataRouteParams { | ||||
|   @ValidateUUID() | ||||
|   id!: string; | ||||
|  | ||||
|   @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) | ||||
|   key!: AssetMetadataKey; | ||||
| } | ||||
|  | ||||
| export class AssetMetadataUpsertDto { | ||||
|   @IsArray() | ||||
|   @ValidateNested({ each: true }) | ||||
|   @Type(() => AssetMetadataUpsertItemDto) | ||||
|   items!: AssetMetadataUpsertItemDto[]; | ||||
| } | ||||
|  | ||||
| export class AssetMetadataUpsertItemDto implements AssetMetadataItem { | ||||
|   @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) | ||||
|   key!: AssetMetadataKey; | ||||
|  | ||||
|   @IsObject() | ||||
|   @ValidateNested() | ||||
|   @Type((options) => { | ||||
|     switch (options?.object.key) { | ||||
|       case AssetMetadataKey.MobileApp: { | ||||
|         return AssetMetadataMobileAppDto; | ||||
|       } | ||||
|       default: { | ||||
|         return Object; | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|   value!: AssetMetadata[AssetMetadataKey]; | ||||
| } | ||||
|  | ||||
| export class AssetMetadataMobileAppDto { | ||||
|   @IsString() | ||||
|   @Optional() | ||||
|   iCloudId?: string; | ||||
| } | ||||
|  | ||||
| export class AssetMetadataResponseDto { | ||||
|   @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) | ||||
|   key!: AssetMetadataKey; | ||||
|   value!: object; | ||||
|   updatedAt!: Date; | ||||
| } | ||||
|  | ||||
| export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { | ||||
|   return { | ||||
|     images: stats[AssetType.Image], | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; | ||||
| import { AssetResponseDto } from 'src/dtos/asset-response.dto'; | ||||
| import { | ||||
|   AlbumUserRole, | ||||
|   AssetMetadataKey, | ||||
|   AssetOrder, | ||||
|   AssetType, | ||||
|   AssetVisibility, | ||||
| @@ -162,6 +163,21 @@ export class SyncAssetExifV1 { | ||||
|   fps!: number | null; | ||||
| } | ||||
|  | ||||
| @ExtraModel() | ||||
| export class SyncAssetMetadataV1 { | ||||
|   assetId!: string; | ||||
|   @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) | ||||
|   key!: AssetMetadataKey; | ||||
|   value!: object; | ||||
| } | ||||
|  | ||||
| @ExtraModel() | ||||
| export class SyncAssetMetadataDeleteV1 { | ||||
|   assetId!: string; | ||||
|   @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) | ||||
|   key!: AssetMetadataKey; | ||||
| } | ||||
|  | ||||
| @ExtraModel() | ||||
| export class SyncAlbumDeleteV1 { | ||||
|   albumId!: string; | ||||
| @@ -328,6 +344,8 @@ export type SyncItem = { | ||||
|   [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; | ||||
|   [SyncEntityType.AssetV1]: SyncAssetV1; | ||||
|   [SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1; | ||||
|   [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; | ||||
|   [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; | ||||
|   [SyncEntityType.AssetExifV1]: SyncAssetExifV1; | ||||
|   [SyncEntityType.PartnerAssetV1]: SyncAssetV1; | ||||
|   [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; | ||||
|   | ||||
| @@ -276,6 +276,10 @@ export enum UserMetadataKey { | ||||
|   Onboarding = 'onboarding', | ||||
| } | ||||
|  | ||||
| export enum AssetMetadataKey { | ||||
|   MobileApp = 'mobile-app', | ||||
| } | ||||
|  | ||||
| export enum UserAvatarColor { | ||||
|   Primary = 'primary', | ||||
|   Pink = 'pink', | ||||
| @@ -627,6 +631,7 @@ export enum SyncRequestType { | ||||
|   AlbumAssetExifsV1 = 'AlbumAssetExifsV1', | ||||
|   AssetsV1 = 'AssetsV1', | ||||
|   AssetExifsV1 = 'AssetExifsV1', | ||||
|   AssetMetadataV1 = 'AssetMetadataV1', | ||||
|   AuthUsersV1 = 'AuthUsersV1', | ||||
|   MemoriesV1 = 'MemoriesV1', | ||||
|   MemoryToAssetsV1 = 'MemoryToAssetsV1', | ||||
| @@ -650,6 +655,8 @@ export enum SyncEntityType { | ||||
|   AssetV1 = 'AssetV1', | ||||
|   AssetDeleteV1 = 'AssetDeleteV1', | ||||
|   AssetExifV1 = 'AssetExifV1', | ||||
|   AssetMetadataV1 = 'AssetMetadataV1', | ||||
|   AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', | ||||
|  | ||||
|   PartnerV1 = 'PartnerV1', | ||||
|   PartnerDeleteV1 = 'PartnerDeleteV1', | ||||
|   | ||||
| @@ -19,6 +19,33 @@ returning | ||||
|   "dateTimeOriginal", | ||||
|   "timeZone" | ||||
|  | ||||
| -- AssetRepository.getMetadata | ||||
| select | ||||
|   "key", | ||||
|   "value", | ||||
|   "updatedAt" | ||||
| from | ||||
|   "asset_metadata" | ||||
| where | ||||
|   "assetId" = $1 | ||||
|  | ||||
| -- AssetRepository.getMetadataByKey | ||||
| select | ||||
|   "key", | ||||
|   "value", | ||||
|   "updatedAt" | ||||
| from | ||||
|   "asset_metadata" | ||||
| where | ||||
|   "assetId" = $1 | ||||
|   and "key" = $2 | ||||
|  | ||||
| -- AssetRepository.deleteMetadataByKey | ||||
| delete from "asset_metadata" | ||||
| where | ||||
|   "assetId" = $1 | ||||
|   and "key" = $2 | ||||
|  | ||||
| -- AssetRepository.getByDayOfYear | ||||
| with | ||||
|   "res" as ( | ||||
|   | ||||
| @@ -539,6 +539,37 @@ where | ||||
| order by | ||||
|   "asset_face"."updateId" asc | ||||
|  | ||||
| -- SyncRepository.assetMetadata.getDeletes | ||||
| select | ||||
|   "asset_metadata_audit"."id", | ||||
|   "assetId", | ||||
|   "key" | ||||
| from | ||||
|   "asset_metadata_audit" as "asset_metadata_audit" | ||||
|   left join "asset" on "asset"."id" = "asset_metadata_audit"."assetId" | ||||
| where | ||||
|   "asset_metadata_audit"."id" < $1 | ||||
|   and "asset_metadata_audit"."id" > $2 | ||||
|   and "asset"."ownerId" = $3 | ||||
| order by | ||||
|   "asset_metadata_audit"."id" asc | ||||
|  | ||||
| -- SyncRepository.assetMetadata.getUpserts | ||||
| select | ||||
|   "assetId", | ||||
|   "key", | ||||
|   "value", | ||||
|   "asset_metadata"."updateId" | ||||
| from | ||||
|   "asset_metadata" as "asset_metadata" | ||||
|   inner join "asset" on "asset"."id" = "asset_metadata"."assetId" | ||||
| where | ||||
|   "asset_metadata"."updateId" < $1 | ||||
|   and "asset_metadata"."updateId" > $2 | ||||
|   and "asset"."ownerId" = $3 | ||||
| order by | ||||
|   "asset_metadata"."updateId" asc | ||||
|  | ||||
| -- SyncRepository.authUser.getUpserts | ||||
| select | ||||
|   "id", | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { Insertable, Kysely, NotNull, Selectable, UpdateResult, Updateable, sql } from 'kysely'; | ||||
| import { Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely'; | ||||
| import { isEmpty, isUndefined, omitBy } from 'lodash'; | ||||
| import { InjectKysely } from 'nestjs-kysely'; | ||||
| import { Stack } from 'src/database'; | ||||
| import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; | ||||
| import { DB } from 'src/schema'; | ||||
| import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; | ||||
| import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | ||||
| import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; | ||||
| import { AssetTable } from 'src/schema/tables/asset.table'; | ||||
| import { AssetMetadataItem } from 'src/types'; | ||||
| import { | ||||
|   anyUuid, | ||||
|   asUuid, | ||||
| @@ -210,6 +211,43 @@ export class AssetRepository { | ||||
|       .execute(); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [DummyValue.UUID] }) | ||||
|   getMetadata(assetId: string) { | ||||
|     return this.db | ||||
|       .selectFrom('asset_metadata') | ||||
|       .select(['key', 'value', 'updatedAt']) | ||||
|       .where('assetId', '=', assetId) | ||||
|       .execute(); | ||||
|   } | ||||
|  | ||||
|   upsertMetadata(id: string, items: AssetMetadataItem[]) { | ||||
|     return this.db | ||||
|       .insertInto('asset_metadata') | ||||
|       .values(items.map((item) => ({ assetId: id, ...item }))) | ||||
|       .onConflict((oc) => | ||||
|         oc | ||||
|           .columns(['assetId', 'key']) | ||||
|           .doUpdateSet((eb) => ({ key: eb.ref('excluded.key'), value: eb.ref('excluded.value') })), | ||||
|       ) | ||||
|       .returning(['key', 'value', 'updatedAt']) | ||||
|       .execute(); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) | ||||
|   getMetadataByKey(assetId: string, key: AssetMetadataKey) { | ||||
|     return this.db | ||||
|       .selectFrom('asset_metadata') | ||||
|       .select(['key', 'value', 'updatedAt']) | ||||
|       .where('assetId', '=', assetId) | ||||
|       .where('key', '=', key) | ||||
|       .executeTakeFirst(); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) | ||||
|   async deleteMetadataByKey(id: string, key: AssetMetadataKey) { | ||||
|     await this.db.deleteFrom('asset_metadata').where('assetId', '=', id).where('key', '=', key).execute(); | ||||
|   } | ||||
|  | ||||
|   create(asset: Insertable<AssetTable>) { | ||||
|     return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); | ||||
|   } | ||||
|   | ||||
| @@ -54,6 +54,7 @@ export class SyncRepository { | ||||
|   asset: AssetSync; | ||||
|   assetExif: AssetExifSync; | ||||
|   assetFace: AssetFaceSync; | ||||
|   assetMetadata: AssetMetadataSync; | ||||
|   authUser: AuthUserSync; | ||||
|   memory: MemorySync; | ||||
|   memoryToAsset: MemoryToAssetSync; | ||||
| @@ -75,6 +76,7 @@ export class SyncRepository { | ||||
|     this.asset = new AssetSync(this.db); | ||||
|     this.assetExif = new AssetExifSync(this.db); | ||||
|     this.assetFace = new AssetFaceSync(this.db); | ||||
|     this.assetMetadata = new AssetMetadataSync(this.db); | ||||
|     this.authUser = new AuthUserSync(this.db); | ||||
|     this.memory = new MemorySync(this.db); | ||||
|     this.memoryToAsset = new MemoryToAssetSync(this.db); | ||||
| @@ -685,3 +687,23 @@ class UserMetadataSync extends BaseSync { | ||||
|       .stream(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AssetMetadataSync extends BaseSync { | ||||
|   @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) | ||||
|   getDeletes(options: SyncQueryOptions, userId: string) { | ||||
|     return this.auditQuery('asset_metadata_audit', options) | ||||
|       .select(['asset_metadata_audit.id', 'assetId', 'key']) | ||||
|       .leftJoin('asset', 'asset.id', 'asset_metadata_audit.assetId') | ||||
|       .where('asset.ownerId', '=', userId) | ||||
|       .stream(); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [dummyQueryOptions, DummyValue.UUID], stream: true }) | ||||
|   getUpserts(options: SyncQueryOptions, userId: string) { | ||||
|     return this.upsertQuery('asset_metadata', options) | ||||
|       .select(['assetId', 'key', 'value', 'asset_metadata.updateId']) | ||||
|       .innerJoin('asset', 'asset.id', 'asset_metadata.assetId') | ||||
|       .where('asset.ownerId', '=', userId) | ||||
|       .stream(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,10 @@ import { columns } from 'src/database'; | ||||
| import { DummyValue, GenerateSql } from 'src/decorators'; | ||||
| import { AssetType, AssetVisibility, UserStatus } from 'src/enum'; | ||||
| import { DB } from 'src/schema'; | ||||
| import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; | ||||
| import { UserTable } from 'src/schema/tables/user.table'; | ||||
| import { UserMetadata, UserMetadataItem } from 'src/types'; | ||||
| import { asUuid } from 'src/utils/database'; | ||||
|  | ||||
| type Upsert = Insertable<UserMetadataTable>; | ||||
|  | ||||
| export interface UserListFilter { | ||||
|   id?: string; | ||||
|   withDeleted?: boolean; | ||||
| @@ -211,12 +208,12 @@ export class UserRepository { | ||||
|   async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { | ||||
|     await this.db | ||||
|       .insertInto('user_metadata') | ||||
|       .values({ userId: id, key, value } as Upsert) | ||||
|       .values({ userId: id, key, value }) | ||||
|       .onConflict((oc) => | ||||
|         oc.columns(['userId', 'key']).doUpdateSet({ | ||||
|           key, | ||||
|           value, | ||||
|         } as Upsert), | ||||
|         }), | ||||
|       ) | ||||
|       .execute(); | ||||
|   } | ||||
|   | ||||
| @@ -230,6 +230,19 @@ export const user_metadata_audit = registerFunction({ | ||||
|     END`, | ||||
| }); | ||||
|  | ||||
| export const asset_metadata_audit = registerFunction({ | ||||
|   name: 'asset_metadata_audit', | ||||
|   returnType: 'TRIGGER', | ||||
|   language: 'PLPGSQL', | ||||
|   body: ` | ||||
|     BEGIN | ||||
|       INSERT INTO asset_metadata_audit ("assetId", "key") | ||||
|       SELECT "assetId", "key" | ||||
|       FROM OLD; | ||||
|       RETURN NULL; | ||||
|     END`, | ||||
| }); | ||||
|  | ||||
| export const asset_face_audit = registerFunction({ | ||||
|   name: 'asset_face_audit', | ||||
|   returnType: 'TRIGGER', | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { | ||||
|   album_user_delete_audit, | ||||
|   asset_delete_audit, | ||||
|   asset_face_audit, | ||||
|   asset_metadata_audit, | ||||
|   f_concat_ws, | ||||
|   f_unaccent, | ||||
|   immich_uuid_v7, | ||||
| @@ -32,6 +33,8 @@ import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table'; | ||||
| import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; | ||||
| import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | ||||
| import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; | ||||
| import { AssetMetadataAuditTable } from 'src/schema/tables/asset-metadata-audit.table'; | ||||
| import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; | ||||
| import { AssetTable } from 'src/schema/tables/asset.table'; | ||||
| import { AuditTable } from 'src/schema/tables/audit.table'; | ||||
| import { FaceSearchTable } from 'src/schema/tables/face-search.table'; | ||||
| @@ -81,6 +84,8 @@ export class ImmichDatabase { | ||||
|     AssetAuditTable, | ||||
|     AssetFaceTable, | ||||
|     AssetFaceAuditTable, | ||||
|     AssetMetadataTable, | ||||
|     AssetMetadataAuditTable, | ||||
|     AssetJobStatusTable, | ||||
|     AssetTable, | ||||
|     AssetFileTable, | ||||
| @@ -135,6 +140,7 @@ export class ImmichDatabase { | ||||
|     stack_delete_audit, | ||||
|     person_delete_audit, | ||||
|     user_metadata_audit, | ||||
|     asset_metadata_audit, | ||||
|     asset_face_audit, | ||||
|   ]; | ||||
|  | ||||
| @@ -164,6 +170,8 @@ export interface DB { | ||||
|   asset_face: AssetFaceTable; | ||||
|   asset_face_audit: AssetFaceAuditTable; | ||||
|   asset_file: AssetFileTable; | ||||
|   asset_metadata: AssetMetadataTable; | ||||
|   asset_metadata_audit: AssetMetadataAuditTable; | ||||
|   asset_job_status: AssetJobStatusTable; | ||||
|   asset_audit: AssetAuditTable; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| import { Kysely, sql } from 'kysely'; | ||||
|  | ||||
| export async function up(db: Kysely<any>): Promise<void> { | ||||
|   await sql`CREATE OR REPLACE FUNCTION asset_metadata_audit() | ||||
|   RETURNS TRIGGER | ||||
|   LANGUAGE PLPGSQL | ||||
|   AS $$ | ||||
|     BEGIN | ||||
|       INSERT INTO asset_metadata_audit ("assetId", "key") | ||||
|       SELECT "assetId", "key" | ||||
|       FROM OLD; | ||||
|       RETURN NULL; | ||||
|     END | ||||
|   $$;`.execute(db); | ||||
|   await sql`CREATE TABLE "asset_metadata_audit" ( | ||||
|   "id" uuid NOT NULL DEFAULT immich_uuid_v7(), | ||||
|   "assetId" uuid NOT NULL, | ||||
|   "key" character varying NOT NULL, | ||||
|   "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), | ||||
|   CONSTRAINT "asset_metadata_audit_pkey" PRIMARY KEY ("id") | ||||
| );`.execute(db); | ||||
|   await sql`CREATE INDEX "asset_metadata_audit_assetId_idx" ON "asset_metadata_audit" ("assetId");`.execute(db); | ||||
|   await sql`CREATE INDEX "asset_metadata_audit_key_idx" ON "asset_metadata_audit" ("key");`.execute(db); | ||||
|   await sql`CREATE INDEX "asset_metadata_audit_deletedAt_idx" ON "asset_metadata_audit" ("deletedAt");`.execute(db); | ||||
|   await sql`CREATE TABLE "asset_metadata" ( | ||||
|   "assetId" uuid NOT NULL, | ||||
|   "key" character varying NOT NULL, | ||||
|   "value" jsonb NOT NULL, | ||||
|   "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), | ||||
|   "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), | ||||
|   CONSTRAINT "asset_metadata_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE CASCADE ON DELETE CASCADE, | ||||
|   CONSTRAINT "asset_metadata_pkey" PRIMARY KEY ("assetId", "key") | ||||
| );`.execute(db); | ||||
|   await sql`CREATE INDEX "asset_metadata_updateId_idx" ON "asset_metadata" ("updateId");`.execute(db); | ||||
|   await sql`CREATE INDEX "asset_metadata_updatedAt_idx" ON "asset_metadata" ("updatedAt");`.execute(db); | ||||
|   await sql`CREATE OR REPLACE TRIGGER "asset_metadata_audit" | ||||
|   AFTER DELETE ON "asset_metadata" | ||||
|   REFERENCING OLD TABLE AS "old" | ||||
|   FOR EACH STATEMENT | ||||
|   WHEN (pg_trigger_depth() = 0) | ||||
|   EXECUTE FUNCTION asset_metadata_audit();`.execute(db); | ||||
|   await sql`CREATE OR REPLACE TRIGGER "asset_metadata_updated_at" | ||||
|   BEFORE UPDATE ON "asset_metadata" | ||||
|   FOR EACH ROW | ||||
|   EXECUTE FUNCTION updated_at();`.execute(db); | ||||
|   await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_metadata_audit', '{"type":"function","name":"asset_metadata_audit","sql":"CREATE OR REPLACE FUNCTION asset_metadata_audit()\\n  RETURNS TRIGGER\\n  LANGUAGE PLPGSQL\\n  AS $$\\n    BEGIN\\n      INSERT INTO asset_metadata_audit (\\"assetId\\", \\"key\\")\\n      SELECT \\"assetId\\", \\"key\\"\\n      FROM OLD;\\n      RETURN NULL;\\n    END\\n  $$;"}'::jsonb);`.execute(db); | ||||
|   await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_audit', '{"type":"trigger","name":"asset_metadata_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_audit\\"\\n  AFTER DELETE ON \\"asset_metadata\\"\\n  REFERENCING OLD TABLE AS \\"old\\"\\n  FOR EACH STATEMENT\\n  WHEN (pg_trigger_depth() = 0)\\n  EXECUTE FUNCTION asset_metadata_audit();"}'::jsonb);`.execute(db); | ||||
|   await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_metadata_updated_at', '{"type":"trigger","name":"asset_metadata_updated_at","sql":"CREATE OR REPLACE TRIGGER \\"asset_metadata_updated_at\\"\\n  BEFORE UPDATE ON \\"asset_metadata\\"\\n  FOR EACH ROW\\n  EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); | ||||
| } | ||||
|  | ||||
| export async function down(db: Kysely<any>): Promise<void> { | ||||
|   await sql`DROP TABLE "asset_metadata_audit";`.execute(db); | ||||
|   await sql`DROP TABLE "asset_metadata";`.execute(db); | ||||
|   await sql`DROP FUNCTION asset_metadata_audit;`.execute(db); | ||||
|   await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_metadata_audit';`.execute(db); | ||||
|   await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_audit';`.execute(db); | ||||
|   await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_metadata_updated_at';`.execute(db); | ||||
| } | ||||
							
								
								
									
										18
									
								
								server/src/schema/tables/asset-metadata-audit.table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								server/src/schema/tables/asset-metadata-audit.table.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; | ||||
| import { AssetMetadataKey } from 'src/enum'; | ||||
| import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; | ||||
|  | ||||
| @Table('asset_metadata_audit') | ||||
| export class AssetMetadataAuditTable { | ||||
|   @PrimaryGeneratedUuidV7Column() | ||||
|   id!: Generated<string>; | ||||
|  | ||||
|   @Column({ type: 'uuid', index: true }) | ||||
|   assetId!: string; | ||||
|  | ||||
|   @Column({ index: true }) | ||||
|   key!: AssetMetadataKey; | ||||
|  | ||||
|   @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) | ||||
|   deletedAt!: Generated<Timestamp>; | ||||
| } | ||||
							
								
								
									
										46
									
								
								server/src/schema/tables/asset-metadata.table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/src/schema/tables/asset-metadata.table.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; | ||||
| import { AssetMetadataKey } from 'src/enum'; | ||||
| import { asset_metadata_audit } from 'src/schema/functions'; | ||||
| import { AssetTable } from 'src/schema/tables/asset.table'; | ||||
| import { | ||||
|   AfterDeleteTrigger, | ||||
|   Column, | ||||
|   ForeignKeyColumn, | ||||
|   Generated, | ||||
|   PrimaryColumn, | ||||
|   Table, | ||||
|   Timestamp, | ||||
|   UpdateDateColumn, | ||||
| } from 'src/sql-tools'; | ||||
| import { AssetMetadata, AssetMetadataItem } from 'src/types'; | ||||
|  | ||||
| @UpdatedAtTrigger('asset_metadata_updated_at') | ||||
| @Table('asset_metadata') | ||||
| @AfterDeleteTrigger({ | ||||
|   scope: 'statement', | ||||
|   function: asset_metadata_audit, | ||||
|   referencingOldTableAs: 'old', | ||||
|   when: 'pg_trigger_depth() = 0', | ||||
| }) | ||||
| export class AssetMetadataTable<T extends keyof AssetMetadata = AssetMetadataKey> implements AssetMetadataItem<T> { | ||||
|   @ForeignKeyColumn(() => AssetTable, { | ||||
|     onUpdate: 'CASCADE', | ||||
|     onDelete: 'CASCADE', | ||||
|     primary: true, | ||||
|     //  [assetId, key] is the PK constraint | ||||
|     index: false, | ||||
|   }) | ||||
|   assetId!: string; | ||||
|  | ||||
|   @PrimaryColumn({ type: 'character varying' }) | ||||
|   key!: T; | ||||
|  | ||||
|   @Column({ type: 'jsonb' }) | ||||
|   value!: AssetMetadata[T]; | ||||
|  | ||||
|   @UpdateIdColumn({ index: true }) | ||||
|   updateId!: Generated<string>; | ||||
|  | ||||
|   @UpdateDateColumn({ index: true }) | ||||
|   updatedAt!: Generated<Timestamp>; | ||||
| } | ||||
| @@ -423,6 +423,10 @@ export class AssetMediaService extends BaseService { | ||||
|       sidecarPath: sidecarFile?.originalPath, | ||||
|     }); | ||||
|  | ||||
|     if (dto.metadata) { | ||||
|       await this.assetRepository.upsertMetadata(asset.id, dto.metadata); | ||||
|     } | ||||
|  | ||||
|     if (sidecarFile) { | ||||
|       await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); | ||||
|     } | ||||
|   | ||||
| @@ -9,12 +9,14 @@ import { | ||||
|   AssetBulkUpdateDto, | ||||
|   AssetJobName, | ||||
|   AssetJobsDto, | ||||
|   AssetMetadataResponseDto, | ||||
|   AssetMetadataUpsertDto, | ||||
|   AssetStatsDto, | ||||
|   UpdateAssetDto, | ||||
|   mapStats, | ||||
| } from 'src/dtos/asset.dto'; | ||||
| import { AuthDto } from 'src/dtos/auth.dto'; | ||||
| import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; | ||||
| import { AssetMetadataKey, AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; | ||||
| import { BaseService } from 'src/services/base.service'; | ||||
| import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; | ||||
| import { requireElevatedPermission } from 'src/utils/access'; | ||||
| @@ -93,7 +95,7 @@ export class AssetService extends BaseService { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); | ||||
|     await this.updateExif({ id, description, dateTimeOriginal, latitude, longitude, rating }); | ||||
|  | ||||
|     const asset = await this.assetRepository.update({ id, ...rest }); | ||||
|  | ||||
| @@ -273,6 +275,31 @@ export class AssetService extends BaseService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async getMetadata(auth: AuthDto, id: string): Promise<AssetMetadataResponseDto[]> { | ||||
|     await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); | ||||
|     return this.assetRepository.getMetadata(id); | ||||
|   } | ||||
|  | ||||
|   async upsertMetadata(auth: AuthDto, id: string, dto: AssetMetadataUpsertDto): Promise<AssetMetadataResponseDto[]> { | ||||
|     await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); | ||||
|     return this.assetRepository.upsertMetadata(id, dto.items); | ||||
|   } | ||||
|  | ||||
|   async getMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise<AssetMetadataResponseDto> { | ||||
|     await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); | ||||
|  | ||||
|     const item = await this.assetRepository.getMetadataByKey(id, key); | ||||
|     if (!item) { | ||||
|       throw new BadRequestException(`Metadata with key "${key}" not found for asset with id "${id}"`); | ||||
|     } | ||||
|     return item; | ||||
|   } | ||||
|  | ||||
|   async deleteMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise<void> { | ||||
|     await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); | ||||
|     return this.assetRepository.deleteMetadataByKey(id, key); | ||||
|   } | ||||
|  | ||||
|   async run(auth: AuthDto, dto: AssetJobsDto) { | ||||
|     await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); | ||||
|  | ||||
| @@ -313,7 +340,7 @@ export class AssetService extends BaseService { | ||||
|     return asset; | ||||
|   } | ||||
|  | ||||
|   private async updateMetadata(dto: ISidecarWriteJob) { | ||||
|   private async updateExif(dto: ISidecarWriteJob) { | ||||
|     const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; | ||||
|     const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); | ||||
|     if (Object.keys(writes).length > 0) { | ||||
|   | ||||
| @@ -74,6 +74,7 @@ export const SYNC_TYPES_ORDER = [ | ||||
|   SyncRequestType.PeopleV1, | ||||
|   SyncRequestType.AssetFacesV1, | ||||
|   SyncRequestType.UserMetadataV1, | ||||
|   SyncRequestType.AssetMetadataV1, | ||||
| ]; | ||||
|  | ||||
| const throwSessionRequired = () => { | ||||
| @@ -156,6 +157,7 @@ export class SyncService extends BaseService { | ||||
|       [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), | ||||
|       [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), | ||||
|       [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), | ||||
|       [SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth), | ||||
|       [SyncRequestType.PartnerAssetExifsV1]: () => | ||||
|         this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id), | ||||
|       [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap), | ||||
| @@ -759,6 +761,33 @@ export class SyncService extends BaseService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async syncAssetMetadataV1( | ||||
|     options: SyncQueryOptions, | ||||
|     response: Writable, | ||||
|     checkpointMap: CheckpointMap, | ||||
|     auth: AuthDto, | ||||
|   ) { | ||||
|     const deleteType = SyncEntityType.AssetMetadataDeleteV1; | ||||
|     const deletes = this.syncRepository.assetMetadata.getDeletes( | ||||
|       { ...options, ack: checkpointMap[deleteType] }, | ||||
|       auth.user.id, | ||||
|     ); | ||||
|  | ||||
|     for await (const { id, ...data } of deletes) { | ||||
|       send(response, { type: deleteType, ids: [id], data }); | ||||
|     } | ||||
|  | ||||
|     const upsertType = SyncEntityType.AssetMetadataV1; | ||||
|     const upserts = this.syncRepository.assetMetadata.getUpserts( | ||||
|       { ...options, ack: checkpointMap[upsertType] }, | ||||
|       auth.user.id, | ||||
|     ); | ||||
|  | ||||
|     for await (const { updateId, ...data } of upserts) { | ||||
|       send(response, { type: upsertType, ids: [updateId], data }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { | ||||
|     const { type, sessionId, createId } = item; | ||||
|     await this.syncCheckpointRepository.upsertAll([ | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { SystemConfig } from 'src/config'; | ||||
| import { VECTOR_EXTENSIONS } from 'src/constants'; | ||||
| import { | ||||
|   AssetMetadataKey, | ||||
|   AssetOrder, | ||||
|   AssetType, | ||||
|   DatabaseSslMode, | ||||
| @@ -465,11 +466,6 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, | ||||
|   [SystemMetadataKey.MemoriesState]: MemoriesState; | ||||
| } | ||||
|  | ||||
| export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = { | ||||
|   key: T; | ||||
|   value: UserMetadata[T]; | ||||
| }; | ||||
|  | ||||
| export interface UserPreferences { | ||||
|   albums: { | ||||
|     defaultAssetOrder: AssetOrder; | ||||
| @@ -514,8 +510,22 @@ export interface UserPreferences { | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = { | ||||
|   key: T; | ||||
|   value: UserMetadata[T]; | ||||
| }; | ||||
|  | ||||
| export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> { | ||||
|   [UserMetadataKey.Preferences]: DeepPartial<UserPreferences>; | ||||
|   [UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string }; | ||||
|   [UserMetadataKey.Onboarding]: { isOnboarded: boolean }; | ||||
| } | ||||
|  | ||||
| export type AssetMetadataItem<T extends keyof AssetMetadata = AssetMetadataKey> = { | ||||
|   key: T; | ||||
|   value: AssetMetadata[T]; | ||||
| }; | ||||
|  | ||||
| export interface AssetMetadata extends Record<AssetMetadataKey, Record<string, any>> { | ||||
|   [AssetMetadataKey.MobileApp]: { iCloudId: string }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										126
									
								
								server/test/medium/specs/sync/sync-asset-metadata.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								server/test/medium/specs/sync/sync-asset-metadata.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| import { Kysely } from 'kysely'; | ||||
| import { AssetMetadataKey, SyncEntityType, SyncRequestType } from 'src/enum'; | ||||
| import { AssetRepository } from 'src/repositories/asset.repository'; | ||||
| import { DB } from 'src/schema'; | ||||
| import { SyncTestContext } from 'test/medium.factory'; | ||||
| import { getKyselyDB } from 'test/utils'; | ||||
|  | ||||
| let defaultDatabase: Kysely<DB>; | ||||
|  | ||||
| const setup = async (db?: Kysely<DB>) => { | ||||
|   const ctx = new SyncTestContext(db || defaultDatabase); | ||||
|   const { auth, user, session } = await ctx.newSyncAuthUser(); | ||||
|   return { auth, user, session, ctx }; | ||||
| }; | ||||
|  | ||||
| beforeAll(async () => { | ||||
|   defaultDatabase = await getKyselyDB(); | ||||
| }); | ||||
|  | ||||
| describe(SyncEntityType.AssetMetadataV1, () => { | ||||
|   it('should detect and sync new asset metadata', async () => { | ||||
|     const { auth, user, ctx } = await setup(); | ||||
|  | ||||
|     const assetRepo = ctx.get(AssetRepository); | ||||
|     const { asset } = await ctx.newAsset({ ownerId: user.id }); | ||||
|     await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); | ||||
|  | ||||
|     const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); | ||||
|     expect(response).toHaveLength(1); | ||||
|     expect(response).toEqual([ | ||||
|       { | ||||
|         ack: expect.any(String), | ||||
|         data: { | ||||
|           key: AssetMetadataKey.MobileApp, | ||||
|           assetId: asset.id, | ||||
|           value: { iCloudId: 'abc123' }, | ||||
|         }, | ||||
|         type: 'AssetMetadataV1', | ||||
|       }, | ||||
|     ]); | ||||
|  | ||||
|     await ctx.syncAckAll(auth, response); | ||||
|     await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); | ||||
|   }); | ||||
|  | ||||
|   it('should update asset metadata', async () => { | ||||
|     const { auth, user, ctx } = await setup(); | ||||
|  | ||||
|     const assetRepo = ctx.get(AssetRepository); | ||||
|     const { asset } = await ctx.newAsset({ ownerId: user.id }); | ||||
|     await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); | ||||
|  | ||||
|     const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); | ||||
|     expect(response).toHaveLength(1); | ||||
|     expect(response).toEqual([ | ||||
|       { | ||||
|         ack: expect.any(String), | ||||
|         data: { | ||||
|           key: AssetMetadataKey.MobileApp, | ||||
|           assetId: asset.id, | ||||
|           value: { iCloudId: 'abc123' }, | ||||
|         }, | ||||
|         type: 'AssetMetadataV1', | ||||
|       }, | ||||
|     ]); | ||||
|  | ||||
|     await ctx.syncAckAll(auth, response); | ||||
|  | ||||
|     await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc456' } }]); | ||||
|  | ||||
|     const updatedResponse = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); | ||||
|     expect(updatedResponse).toEqual([ | ||||
|       { | ||||
|         ack: expect.any(String), | ||||
|         data: { | ||||
|           key: AssetMetadataKey.MobileApp, | ||||
|           assetId: asset.id, | ||||
|           value: { iCloudId: 'abc456' }, | ||||
|         }, | ||||
|         type: 'AssetMetadataV1', | ||||
|       }, | ||||
|     ]); | ||||
|  | ||||
|     await ctx.syncAckAll(auth, updatedResponse); | ||||
|     await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([]); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| describe(SyncEntityType.AssetMetadataDeleteV1, () => { | ||||
|   it('should delete and sync asset metadata', async () => { | ||||
|     const { auth, user, ctx } = await setup(); | ||||
|  | ||||
|     const assetRepo = ctx.get(AssetRepository); | ||||
|     const { asset } = await ctx.newAsset({ ownerId: user.id }); | ||||
|     await assetRepo.upsertMetadata(asset.id, [{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'abc123' } }]); | ||||
|  | ||||
|     const response = await ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1]); | ||||
|     expect(response).toHaveLength(1); | ||||
|     expect(response).toEqual([ | ||||
|       { | ||||
|         ack: expect.any(String), | ||||
|         data: { | ||||
|           key: AssetMetadataKey.MobileApp, | ||||
|           assetId: asset.id, | ||||
|           value: { iCloudId: 'abc123' }, | ||||
|         }, | ||||
|         type: 'AssetMetadataV1', | ||||
|       }, | ||||
|     ]); | ||||
|  | ||||
|     await ctx.syncAckAll(auth, response); | ||||
|  | ||||
|     await assetRepo.deleteMetadataByKey(asset.id, AssetMetadataKey.MobileApp); | ||||
|  | ||||
|     await expect(ctx.syncStream(auth, [SyncRequestType.AssetMetadataV1])).resolves.toEqual([ | ||||
|       { | ||||
|         ack: expect.any(String), | ||||
|         data: { | ||||
|           assetId: asset.id, | ||||
|           key: AssetMetadataKey.MobileApp, | ||||
|         }, | ||||
|         type: 'AssetMetadataDeleteV1', | ||||
|       }, | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -41,5 +41,9 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi | ||||
|     filterNewExternalAssetPaths: vitest.fn(), | ||||
|     updateByLibraryId: vitest.fn(), | ||||
|     getFileSamples: vitest.fn(), | ||||
|     getMetadata: vitest.fn(), | ||||
|     upsertMetadata: vitest.fn(), | ||||
|     getMetadataByKey: vitest.fn(), | ||||
|     deleteMetadataByKey: vitest.fn(), | ||||
|   }; | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user