mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-01 02:17:43 +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} |  | *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |  | ||||||
| *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload | *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | checkBulkUpload | ||||||
| *AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | checkExistingAssets | *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* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |  | ||||||
| *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |  | *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |  | ||||||
| *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId | *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | getAllUserAssetsByDeviceId | ||||||
| *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |  | *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* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |  | ||||||
| *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |  | *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |  | ||||||
| *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |  | *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |  | ||||||
| *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset | *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset | ||||||
| *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |  | *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |  | ||||||
| *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |  | *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* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |  | ||||||
| *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |  | *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets |  | ||||||
| *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |  | *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail |  | ||||||
| @@ -328,6 +332,10 @@ Class | Method | HTTP request | Description | |||||||
|  - [AssetMediaResponseDto](doc//AssetMediaResponseDto.md) |  - [AssetMediaResponseDto](doc//AssetMediaResponseDto.md) | ||||||
|  - [AssetMediaSize](doc//AssetMediaSize.md) |  - [AssetMediaSize](doc//AssetMediaSize.md) | ||||||
|  - [AssetMediaStatus](doc//AssetMediaStatus.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) |  - [AssetOrder](doc//AssetOrder.md) | ||||||
|  - [AssetResponseDto](doc//AssetResponseDto.md) |  - [AssetResponseDto](doc//AssetResponseDto.md) | ||||||
|  - [AssetStackResponseDto](doc//AssetStackResponseDto.md) |  - [AssetStackResponseDto](doc//AssetStackResponseDto.md) | ||||||
| @@ -485,6 +493,8 @@ Class | Method | HTTP request | Description | |||||||
|  - [SyncAssetExifV1](doc//SyncAssetExifV1.md) |  - [SyncAssetExifV1](doc//SyncAssetExifV1.md) | ||||||
|  - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) |  - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) | ||||||
|  - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) |  - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) | ||||||
|  |  - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) | ||||||
|  |  - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) | ||||||
|  - [SyncAssetV1](doc//SyncAssetV1.md) |  - [SyncAssetV1](doc//SyncAssetV1.md) | ||||||
|  - [SyncAuthUserV1](doc//SyncAuthUserV1.md) |  - [SyncAuthUserV1](doc//SyncAuthUserV1.md) | ||||||
|  - [SyncEntityType](doc//SyncEntityType.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_response_dto.dart'; | ||||||
| part 'model/asset_media_size.dart'; | part 'model/asset_media_size.dart'; | ||||||
| part 'model/asset_media_status.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_order.dart'; | ||||||
| part 'model/asset_response_dto.dart'; | part 'model/asset_response_dto.dart'; | ||||||
| part 'model/asset_stack_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_exif_v1.dart'; | ||||||
| part 'model/sync_asset_face_delete_v1.dart'; | part 'model/sync_asset_face_delete_v1.dart'; | ||||||
| part 'model/sync_asset_face_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_asset_v1.dart'; | ||||||
| part 'model/sync_auth_user_v1.dart'; | part 'model/sync_auth_user_v1.dart'; | ||||||
| part 'model/sync_entity_type.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; |     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. |   /// This endpoint requires the `asset.delete` permission. | ||||||
|   /// |   /// | ||||||
|   /// Note: This method returns the HTTP [Response]. |   /// Note: This method returns the HTTP [Response]. | ||||||
| @@ -368,6 +418,120 @@ class AssetsApi { | |||||||
|     return null; |     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. |   /// This endpoint requires the `asset.statistics` permission. | ||||||
|   /// |   /// | ||||||
|   /// Note: This method returns the HTTP [Response]. |   /// Note: This method returns the HTTP [Response]. | ||||||
| @@ -795,6 +959,66 @@ class AssetsApi { | |||||||
|     return null; |     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. |   /// This endpoint requires the `asset.update` permission. | ||||||
|   /// |   /// | ||||||
|   /// Note: This method returns the HTTP [Response]. |   /// Note: This method returns the HTTP [Response]. | ||||||
| @@ -855,6 +1079,8 @@ class AssetsApi { | |||||||
|   /// |   /// | ||||||
|   /// * [DateTime] fileModifiedAt (required): |   /// * [DateTime] fileModifiedAt (required): | ||||||
|   /// |   /// | ||||||
|  |   /// * [List<AssetMetadataUpsertItemDto>] metadata (required): | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   /// |   /// | ||||||
|   /// * [String] slug: |   /// * [String] slug: | ||||||
| @@ -873,7 +1099,7 @@ class AssetsApi { | |||||||
|   /// * [MultipartFile] sidecarData: |   /// * [MultipartFile] sidecarData: | ||||||
|   /// |   /// | ||||||
|   /// * [AssetVisibility] visibility: |   /// * [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 |     // ignore: prefer_const_declarations | ||||||
|     final apiPath = r'/assets'; |     final apiPath = r'/assets'; | ||||||
| 
 | 
 | ||||||
| @@ -936,6 +1162,10 @@ class AssetsApi { | |||||||
|       hasFields = true; |       hasFields = true; | ||||||
|       mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); |       mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); | ||||||
|     } |     } | ||||||
|  |     if (metadata != null) { | ||||||
|  |       hasFields = true; | ||||||
|  |       mp.fields[r'metadata'] = parameterToString(metadata); | ||||||
|  |     } | ||||||
|     if (sidecarData != null) { |     if (sidecarData != null) { | ||||||
|       hasFields = true; |       hasFields = true; | ||||||
|       mp.fields[r'sidecarData'] = sidecarData.field; |       mp.fields[r'sidecarData'] = sidecarData.field; | ||||||
| @@ -974,6 +1204,8 @@ class AssetsApi { | |||||||
|   /// |   /// | ||||||
|   /// * [DateTime] fileModifiedAt (required): |   /// * [DateTime] fileModifiedAt (required): | ||||||
|   /// |   /// | ||||||
|  |   /// * [List<AssetMetadataUpsertItemDto>] metadata (required): | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   /// |   /// | ||||||
|   /// * [String] slug: |   /// * [String] slug: | ||||||
| @@ -992,8 +1224,8 @@ class AssetsApi { | |||||||
|   /// * [MultipartFile] sidecarData: |   /// * [MultipartFile] sidecarData: | ||||||
|   /// |   /// | ||||||
|   /// * [AssetVisibility] visibility: |   /// * [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 { |   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,  key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); |     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) { |     if (response.statusCode >= HttpStatus.badRequest) { | ||||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); |       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); |           return AssetMediaSizeTypeTransformer().decode(value); | ||||||
|         case 'AssetMediaStatus': |         case 'AssetMediaStatus': | ||||||
|           return AssetMediaStatusTypeTransformer().decode(value); |           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': |         case 'AssetOrder': | ||||||
|           return AssetOrderTypeTransformer().decode(value); |           return AssetOrderTypeTransformer().decode(value); | ||||||
|         case 'AssetResponseDto': |         case 'AssetResponseDto': | ||||||
| @@ -580,6 +588,10 @@ class ApiClient { | |||||||
|           return SyncAssetFaceDeleteV1.fromJson(value); |           return SyncAssetFaceDeleteV1.fromJson(value); | ||||||
|         case 'SyncAssetFaceV1': |         case 'SyncAssetFaceV1': | ||||||
|           return SyncAssetFaceV1.fromJson(value); |           return SyncAssetFaceV1.fromJson(value); | ||||||
|  |         case 'SyncAssetMetadataDeleteV1': | ||||||
|  |           return SyncAssetMetadataDeleteV1.fromJson(value); | ||||||
|  |         case 'SyncAssetMetadataV1': | ||||||
|  |           return SyncAssetMetadataV1.fromJson(value); | ||||||
|         case 'SyncAssetV1': |         case 'SyncAssetV1': | ||||||
|           return SyncAssetV1.fromJson(value); |           return SyncAssetV1.fromJson(value); | ||||||
|         case 'SyncAuthUserV1': |         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) { |   if (value is AssetMediaStatus) { | ||||||
|     return AssetMediaStatusTypeTransformer().encode(value).toString(); |     return AssetMediaStatusTypeTransformer().encode(value).toString(); | ||||||
|   } |   } | ||||||
|  |   if (value is AssetMetadataKey) { | ||||||
|  |     return AssetMetadataKeyTypeTransformer().encode(value).toString(); | ||||||
|  |   } | ||||||
|   if (value is AssetOrder) { |   if (value is AssetOrder) { | ||||||
|     return AssetOrderTypeTransformer().encode(value).toString(); |     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 assetV1 = SyncEntityType._(r'AssetV1'); | ||||||
|   static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); |   static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); | ||||||
|   static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); |   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 partnerV1 = SyncEntityType._(r'PartnerV1'); | ||||||
|   static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); |   static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); | ||||||
|   static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); |   static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); | ||||||
| @@ -76,6 +78,8 @@ class SyncEntityType { | |||||||
|     assetV1, |     assetV1, | ||||||
|     assetDeleteV1, |     assetDeleteV1, | ||||||
|     assetExifV1, |     assetExifV1, | ||||||
|  |     assetMetadataV1, | ||||||
|  |     assetMetadataDeleteV1, | ||||||
|     partnerV1, |     partnerV1, | ||||||
|     partnerDeleteV1, |     partnerDeleteV1, | ||||||
|     partnerAssetV1, |     partnerAssetV1, | ||||||
| @@ -158,6 +162,8 @@ class SyncEntityTypeTypeTransformer { | |||||||
|         case r'AssetV1': return SyncEntityType.assetV1; |         case r'AssetV1': return SyncEntityType.assetV1; | ||||||
|         case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; |         case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; | ||||||
|         case r'AssetExifV1': return SyncEntityType.assetExifV1; |         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'PartnerV1': return SyncEntityType.partnerV1; | ||||||
|         case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; |         case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; | ||||||
|         case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; |         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 albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); | ||||||
|   static const assetsV1 = SyncRequestType._(r'AssetsV1'); |   static const assetsV1 = SyncRequestType._(r'AssetsV1'); | ||||||
|   static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); |   static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); | ||||||
|  |   static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); | ||||||
|   static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); |   static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); | ||||||
|   static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); |   static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); | ||||||
|   static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); |   static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); | ||||||
| @@ -52,6 +53,7 @@ class SyncRequestType { | |||||||
|     albumAssetExifsV1, |     albumAssetExifsV1, | ||||||
|     assetsV1, |     assetsV1, | ||||||
|     assetExifsV1, |     assetExifsV1, | ||||||
|  |     assetMetadataV1, | ||||||
|     authUsersV1, |     authUsersV1, | ||||||
|     memoriesV1, |     memoriesV1, | ||||||
|     memoryToAssetsV1, |     memoryToAssetsV1, | ||||||
| @@ -109,6 +111,7 @@ class SyncRequestTypeTypeTransformer { | |||||||
|         case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; |         case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; | ||||||
|         case r'AssetsV1': return SyncRequestType.assetsV1; |         case r'AssetsV1': return SyncRequestType.assetsV1; | ||||||
|         case r'AssetExifsV1': return SyncRequestType.assetExifsV1; |         case r'AssetExifsV1': return SyncRequestType.assetExifsV1; | ||||||
|  |         case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; | ||||||
|         case r'AuthUsersV1': return SyncRequestType.authUsersV1; |         case r'AuthUsersV1': return SyncRequestType.authUsersV1; | ||||||
|         case r'MemoriesV1': return SyncRequestType.memoriesV1; |         case r'MemoriesV1': return SyncRequestType.memoriesV1; | ||||||
|         case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; |         case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; | ||||||
|   | |||||||
| @@ -2245,6 +2245,203 @@ | |||||||
|         "description": "This endpoint requires the `asset.update` permission." |         "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": { |     "/assets/{id}/original": { | ||||||
|       "get": { |       "get": { | ||||||
|         "operationId": "downloadAsset", |         "operationId": "downloadAsset", | ||||||
| @@ -10615,6 +10812,12 @@ | |||||||
|             "format": "uuid", |             "format": "uuid", | ||||||
|             "type": "string" |             "type": "string" | ||||||
|           }, |           }, | ||||||
|  |           "metadata": { | ||||||
|  |             "items": { | ||||||
|  |               "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" | ||||||
|  |             }, | ||||||
|  |             "type": "array" | ||||||
|  |           }, | ||||||
|           "sidecarData": { |           "sidecarData": { | ||||||
|             "format": "binary", |             "format": "binary", | ||||||
|             "type": "string" |             "type": "string" | ||||||
| @@ -10632,7 +10835,8 @@ | |||||||
|           "deviceAssetId", |           "deviceAssetId", | ||||||
|           "deviceId", |           "deviceId", | ||||||
|           "fileCreatedAt", |           "fileCreatedAt", | ||||||
|           "fileModifiedAt" |           "fileModifiedAt", | ||||||
|  |           "metadata" | ||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
| @@ -10707,6 +10911,69 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "string" |         "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": { |       "AssetOrder": { | ||||||
|         "enum": [ |         "enum": [ | ||||||
|           "asc", |           "asc", | ||||||
| @@ -14944,6 +15211,48 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "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": { |       "SyncAssetV1": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "checksum": { |           "checksum": { | ||||||
| @@ -15114,6 +15423,8 @@ | |||||||
|           "AssetV1", |           "AssetV1", | ||||||
|           "AssetDeleteV1", |           "AssetDeleteV1", | ||||||
|           "AssetExifV1", |           "AssetExifV1", | ||||||
|  |           "AssetMetadataV1", | ||||||
|  |           "AssetMetadataDeleteV1", | ||||||
|           "PartnerV1", |           "PartnerV1", | ||||||
|           "PartnerDeleteV1", |           "PartnerDeleteV1", | ||||||
|           "PartnerAssetV1", |           "PartnerAssetV1", | ||||||
| @@ -15373,6 +15684,7 @@ | |||||||
|           "AlbumAssetExifsV1", |           "AlbumAssetExifsV1", | ||||||
|           "AssetsV1", |           "AssetsV1", | ||||||
|           "AssetExifsV1", |           "AssetExifsV1", | ||||||
|  |           "AssetMetadataV1", | ||||||
|           "AuthUsersV1", |           "AuthUsersV1", | ||||||
|           "MemoriesV1", |           "MemoriesV1", | ||||||
|           "MemoryToAssetsV1", |           "MemoryToAssetsV1", | ||||||
|   | |||||||
| @@ -447,6 +447,10 @@ export type AssetBulkDeleteDto = { | |||||||
|     force?: boolean; |     force?: boolean; | ||||||
|     ids: string[]; |     ids: string[]; | ||||||
| }; | }; | ||||||
|  | export type AssetMetadataUpsertItemDto = { | ||||||
|  |     key: AssetMetadataKey; | ||||||
|  |     value: object; | ||||||
|  | }; | ||||||
| export type AssetMediaCreateDto = { | export type AssetMediaCreateDto = { | ||||||
|     assetData: Blob; |     assetData: Blob; | ||||||
|     deviceAssetId: string; |     deviceAssetId: string; | ||||||
| @@ -457,6 +461,7 @@ export type AssetMediaCreateDto = { | |||||||
|     filename?: string; |     filename?: string; | ||||||
|     isFavorite?: boolean; |     isFavorite?: boolean; | ||||||
|     livePhotoVideoId?: string; |     livePhotoVideoId?: string; | ||||||
|  |     metadata: AssetMetadataUpsertItemDto[]; | ||||||
|     sidecarData?: Blob; |     sidecarData?: Blob; | ||||||
|     visibility?: AssetVisibility; |     visibility?: AssetVisibility; | ||||||
| }; | }; | ||||||
| @@ -516,6 +521,14 @@ export type UpdateAssetDto = { | |||||||
|     rating?: number; |     rating?: number; | ||||||
|     visibility?: AssetVisibility; |     visibility?: AssetVisibility; | ||||||
| }; | }; | ||||||
|  | export type AssetMetadataResponseDto = { | ||||||
|  |     key: AssetMetadataKey; | ||||||
|  |     updatedAt: string; | ||||||
|  |     value: object; | ||||||
|  | }; | ||||||
|  | export type AssetMetadataUpsertDto = { | ||||||
|  |     items: AssetMetadataUpsertItemDto[]; | ||||||
|  | }; | ||||||
| export type AssetMediaReplaceDto = { | export type AssetMediaReplaceDto = { | ||||||
|     assetData: Blob; |     assetData: Blob; | ||||||
|     deviceAssetId: string; |     deviceAssetId: string; | ||||||
| @@ -2273,6 +2286,61 @@ export function updateAsset({ id, updateAssetDto }: { | |||||||
|         body: 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. |  * This endpoint requires the `asset.download` permission. | ||||||
|  */ |  */ | ||||||
| @@ -4725,6 +4793,9 @@ export enum Permission { | |||||||
|     AdminUserDelete = "adminUser.delete", |     AdminUserDelete = "adminUser.delete", | ||||||
|     AdminAuthUnlinkAll = "adminAuth.unlinkAll" |     AdminAuthUnlinkAll = "adminAuth.unlinkAll" | ||||||
| } | } | ||||||
|  | export enum AssetMetadataKey { | ||||||
|  |     MobileApp = "mobile-app" | ||||||
|  | } | ||||||
| export enum AssetMediaStatus { | export enum AssetMediaStatus { | ||||||
|     Created = "created", |     Created = "created", | ||||||
|     Replaced = "replaced", |     Replaced = "replaced", | ||||||
| @@ -4811,6 +4882,8 @@ export enum SyncEntityType { | |||||||
|     AssetV1 = "AssetV1", |     AssetV1 = "AssetV1", | ||||||
|     AssetDeleteV1 = "AssetDeleteV1", |     AssetDeleteV1 = "AssetDeleteV1", | ||||||
|     AssetExifV1 = "AssetExifV1", |     AssetExifV1 = "AssetExifV1", | ||||||
|  |     AssetMetadataV1 = "AssetMetadataV1", | ||||||
|  |     AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", | ||||||
|     PartnerV1 = "PartnerV1", |     PartnerV1 = "PartnerV1", | ||||||
|     PartnerDeleteV1 = "PartnerDeleteV1", |     PartnerDeleteV1 = "PartnerDeleteV1", | ||||||
|     PartnerAssetV1 = "PartnerAssetV1", |     PartnerAssetV1 = "PartnerAssetV1", | ||||||
| @@ -4858,6 +4931,7 @@ export enum SyncRequestType { | |||||||
|     AlbumAssetExifsV1 = "AlbumAssetExifsV1", |     AlbumAssetExifsV1 = "AlbumAssetExifsV1", | ||||||
|     AssetsV1 = "AssetsV1", |     AssetsV1 = "AssetsV1", | ||||||
|     AssetExifsV1 = "AssetExifsV1", |     AssetExifsV1 = "AssetExifsV1", | ||||||
|  |     AssetMetadataV1 = "AssetMetadataV1", | ||||||
|     AuthUsersV1 = "AuthUsersV1", |     AuthUsersV1 = "AuthUsersV1", | ||||||
|     MemoriesV1 = "MemoriesV1", |     MemoriesV1 = "MemoriesV1", | ||||||
|     MemoryToAssetsV1 = "MemoryToAssetsV1", |     MemoryToAssetsV1 = "MemoryToAssetsV1", | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { AssetController } from 'src/controllers/asset.controller'; | import { AssetController } from 'src/controllers/asset.controller'; | ||||||
|  | import { AssetMetadataKey } from 'src/enum'; | ||||||
| import { AssetService } from 'src/services/asset.service'; | import { AssetService } from 'src/services/asset.service'; | ||||||
| import request from 'supertest'; | import request from 'supertest'; | ||||||
| import { factory } from 'test/small.factory'; | import { factory } from 'test/small.factory'; | ||||||
| @@ -6,14 +7,16 @@ import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils' | |||||||
|  |  | ||||||
| describe(AssetController.name, () => { | describe(AssetController.name, () => { | ||||||
|   let ctx: ControllerContext; |   let ctx: ControllerContext; | ||||||
|  |   const service = mockBaseService(AssetService); | ||||||
|  |  | ||||||
|   beforeAll(async () => { |   beforeAll(async () => { | ||||||
|     ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]); |     ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: service }]); | ||||||
|     return () => ctx.close(); |     return () => ctx.close(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|     ctx.reset(); |     ctx.reset(); | ||||||
|  |     service.resetAllMocks(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('PUT /assets', () => { |   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, |   AssetBulkDeleteDto, | ||||||
|   AssetBulkUpdateDto, |   AssetBulkUpdateDto, | ||||||
|   AssetJobsDto, |   AssetJobsDto, | ||||||
|  |   AssetMetadataResponseDto, | ||||||
|  |   AssetMetadataRouteParams, | ||||||
|  |   AssetMetadataUpsertDto, | ||||||
|   AssetStatsDto, |   AssetStatsDto, | ||||||
|   AssetStatsResponseDto, |   AssetStatsResponseDto, | ||||||
|   DeviceIdDto, |   DeviceIdDto, | ||||||
| @@ -85,4 +88,36 @@ export class AssetController { | |||||||
|   ): Promise<AssetResponseDto> { |   ): Promise<AssetResponseDto> { | ||||||
|     return this.service.update(auth, id, dto); |     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 { ApiProperty } from '@nestjs/swagger'; | ||||||
| import { Type } from 'class-transformer'; | import { Type } from 'class-transformer'; | ||||||
| import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; | ||||||
|  | import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; | ||||||
| import { AssetVisibility } from 'src/enum'; | import { AssetVisibility } from 'src/enum'; | ||||||
| import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; | import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; | ||||||
|  |  | ||||||
| @@ -64,6 +65,12 @@ export class AssetMediaCreateDto extends AssetMediaBase { | |||||||
|   @ValidateUUID({ optional: true }) |   @ValidateUUID({ optional: true }) | ||||||
|   livePhotoVideoId?: string; |   livePhotoVideoId?: string; | ||||||
|  |  | ||||||
|  |   @Optional() | ||||||
|  |   @IsArray() | ||||||
|  |   @ValidateNested({ each: true }) | ||||||
|  |   @Type(() => AssetMetadataUpsertItemDto) | ||||||
|  |   metadata!: AssetMetadataUpsertItemDto[]; | ||||||
|  |  | ||||||
|   @ApiProperty({ type: 'string', format: 'binary', required: false }) |   @ApiProperty({ type: 'string', format: 'binary', required: false }) | ||||||
|   [UploadFieldName.SIDECAR_DATA]?: any; |   [UploadFieldName.SIDECAR_DATA]?: any; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,21 +1,25 @@ | |||||||
| import { ApiProperty } from '@nestjs/swagger'; | import { ApiProperty } from '@nestjs/swagger'; | ||||||
| import { Type } from 'class-transformer'; | import { Type } from 'class-transformer'; | ||||||
| import { | import { | ||||||
|  |   IsArray, | ||||||
|   IsDateString, |   IsDateString, | ||||||
|   IsInt, |   IsInt, | ||||||
|   IsLatitude, |   IsLatitude, | ||||||
|   IsLongitude, |   IsLongitude, | ||||||
|   IsNotEmpty, |   IsNotEmpty, | ||||||
|  |   IsObject, | ||||||
|   IsPositive, |   IsPositive, | ||||||
|   IsString, |   IsString, | ||||||
|   IsTimeZone, |   IsTimeZone, | ||||||
|   Max, |   Max, | ||||||
|   Min, |   Min, | ||||||
|   ValidateIf, |   ValidateIf, | ||||||
|  |   ValidateNested, | ||||||
| } from 'class-validator'; | } from 'class-validator'; | ||||||
| import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; | 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 { AssetStats } from 'src/repositories/asset.repository'; | ||||||
|  | import { AssetMetadata, AssetMetadataItem } from 'src/types'; | ||||||
| import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; | import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; | ||||||
|  |  | ||||||
| export class DeviceIdDto { | export class DeviceIdDto { | ||||||
| @@ -135,6 +139,53 @@ export class AssetStatsResponseDto { | |||||||
|   total!: number; |   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 => { | export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { | ||||||
|   return { |   return { | ||||||
|     images: stats[AssetType.Image], |     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 { AssetResponseDto } from 'src/dtos/asset-response.dto'; | ||||||
| import { | import { | ||||||
|   AlbumUserRole, |   AlbumUserRole, | ||||||
|  |   AssetMetadataKey, | ||||||
|   AssetOrder, |   AssetOrder, | ||||||
|   AssetType, |   AssetType, | ||||||
|   AssetVisibility, |   AssetVisibility, | ||||||
| @@ -162,6 +163,21 @@ export class SyncAssetExifV1 { | |||||||
|   fps!: number | null; |   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() | @ExtraModel() | ||||||
| export class SyncAlbumDeleteV1 { | export class SyncAlbumDeleteV1 { | ||||||
|   albumId!: string; |   albumId!: string; | ||||||
| @@ -328,6 +344,8 @@ export type SyncItem = { | |||||||
|   [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; |   [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; | ||||||
|   [SyncEntityType.AssetV1]: SyncAssetV1; |   [SyncEntityType.AssetV1]: SyncAssetV1; | ||||||
|   [SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1; |   [SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1; | ||||||
|  |   [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; | ||||||
|  |   [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; | ||||||
|   [SyncEntityType.AssetExifV1]: SyncAssetExifV1; |   [SyncEntityType.AssetExifV1]: SyncAssetExifV1; | ||||||
|   [SyncEntityType.PartnerAssetV1]: SyncAssetV1; |   [SyncEntityType.PartnerAssetV1]: SyncAssetV1; | ||||||
|   [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; |   [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; | ||||||
|   | |||||||
| @@ -276,6 +276,10 @@ export enum UserMetadataKey { | |||||||
|   Onboarding = 'onboarding', |   Onboarding = 'onboarding', | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum AssetMetadataKey { | ||||||
|  |   MobileApp = 'mobile-app', | ||||||
|  | } | ||||||
|  |  | ||||||
| export enum UserAvatarColor { | export enum UserAvatarColor { | ||||||
|   Primary = 'primary', |   Primary = 'primary', | ||||||
|   Pink = 'pink', |   Pink = 'pink', | ||||||
| @@ -627,6 +631,7 @@ export enum SyncRequestType { | |||||||
|   AlbumAssetExifsV1 = 'AlbumAssetExifsV1', |   AlbumAssetExifsV1 = 'AlbumAssetExifsV1', | ||||||
|   AssetsV1 = 'AssetsV1', |   AssetsV1 = 'AssetsV1', | ||||||
|   AssetExifsV1 = 'AssetExifsV1', |   AssetExifsV1 = 'AssetExifsV1', | ||||||
|  |   AssetMetadataV1 = 'AssetMetadataV1', | ||||||
|   AuthUsersV1 = 'AuthUsersV1', |   AuthUsersV1 = 'AuthUsersV1', | ||||||
|   MemoriesV1 = 'MemoriesV1', |   MemoriesV1 = 'MemoriesV1', | ||||||
|   MemoryToAssetsV1 = 'MemoryToAssetsV1', |   MemoryToAssetsV1 = 'MemoryToAssetsV1', | ||||||
| @@ -650,6 +655,8 @@ export enum SyncEntityType { | |||||||
|   AssetV1 = 'AssetV1', |   AssetV1 = 'AssetV1', | ||||||
|   AssetDeleteV1 = 'AssetDeleteV1', |   AssetDeleteV1 = 'AssetDeleteV1', | ||||||
|   AssetExifV1 = 'AssetExifV1', |   AssetExifV1 = 'AssetExifV1', | ||||||
|  |   AssetMetadataV1 = 'AssetMetadataV1', | ||||||
|  |   AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', | ||||||
|  |  | ||||||
|   PartnerV1 = 'PartnerV1', |   PartnerV1 = 'PartnerV1', | ||||||
|   PartnerDeleteV1 = 'PartnerDeleteV1', |   PartnerDeleteV1 = 'PartnerDeleteV1', | ||||||
|   | |||||||
| @@ -19,6 +19,33 @@ returning | |||||||
|   "dateTimeOriginal", |   "dateTimeOriginal", | ||||||
|   "timeZone" |   "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 | -- AssetRepository.getByDayOfYear | ||||||
| with | with | ||||||
|   "res" as ( |   "res" as ( | ||||||
|   | |||||||
| @@ -539,6 +539,37 @@ where | |||||||
| order by | order by | ||||||
|   "asset_face"."updateId" asc |   "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 | -- SyncRepository.authUser.getUpserts | ||||||
| select | select | ||||||
|   "id", |   "id", | ||||||
|   | |||||||
| @@ -1,15 +1,16 @@ | |||||||
| import { Injectable } from '@nestjs/common'; | 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 { isEmpty, isUndefined, omitBy } from 'lodash'; | ||||||
| import { InjectKysely } from 'nestjs-kysely'; | import { InjectKysely } from 'nestjs-kysely'; | ||||||
| import { Stack } from 'src/database'; | import { Stack } from 'src/database'; | ||||||
| import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; | 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 { DB } from 'src/schema'; | ||||||
| import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; | import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; | ||||||
| import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | ||||||
| import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; | import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; | ||||||
| import { AssetTable } from 'src/schema/tables/asset.table'; | import { AssetTable } from 'src/schema/tables/asset.table'; | ||||||
|  | import { AssetMetadataItem } from 'src/types'; | ||||||
| import { | import { | ||||||
|   anyUuid, |   anyUuid, | ||||||
|   asUuid, |   asUuid, | ||||||
| @@ -210,6 +211,43 @@ export class AssetRepository { | |||||||
|       .execute(); |       .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>) { |   create(asset: Insertable<AssetTable>) { | ||||||
|     return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); |     return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -54,6 +54,7 @@ export class SyncRepository { | |||||||
|   asset: AssetSync; |   asset: AssetSync; | ||||||
|   assetExif: AssetExifSync; |   assetExif: AssetExifSync; | ||||||
|   assetFace: AssetFaceSync; |   assetFace: AssetFaceSync; | ||||||
|  |   assetMetadata: AssetMetadataSync; | ||||||
|   authUser: AuthUserSync; |   authUser: AuthUserSync; | ||||||
|   memory: MemorySync; |   memory: MemorySync; | ||||||
|   memoryToAsset: MemoryToAssetSync; |   memoryToAsset: MemoryToAssetSync; | ||||||
| @@ -75,6 +76,7 @@ export class SyncRepository { | |||||||
|     this.asset = new AssetSync(this.db); |     this.asset = new AssetSync(this.db); | ||||||
|     this.assetExif = new AssetExifSync(this.db); |     this.assetExif = new AssetExifSync(this.db); | ||||||
|     this.assetFace = new AssetFaceSync(this.db); |     this.assetFace = new AssetFaceSync(this.db); | ||||||
|  |     this.assetMetadata = new AssetMetadataSync(this.db); | ||||||
|     this.authUser = new AuthUserSync(this.db); |     this.authUser = new AuthUserSync(this.db); | ||||||
|     this.memory = new MemorySync(this.db); |     this.memory = new MemorySync(this.db); | ||||||
|     this.memoryToAsset = new MemoryToAssetSync(this.db); |     this.memoryToAsset = new MemoryToAssetSync(this.db); | ||||||
| @@ -685,3 +687,23 @@ class UserMetadataSync extends BaseSync { | |||||||
|       .stream(); |       .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 { DummyValue, GenerateSql } from 'src/decorators'; | ||||||
| import { AssetType, AssetVisibility, UserStatus } from 'src/enum'; | import { AssetType, AssetVisibility, UserStatus } from 'src/enum'; | ||||||
| import { DB } from 'src/schema'; | import { DB } from 'src/schema'; | ||||||
| import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; |  | ||||||
| import { UserTable } from 'src/schema/tables/user.table'; | import { UserTable } from 'src/schema/tables/user.table'; | ||||||
| import { UserMetadata, UserMetadataItem } from 'src/types'; | import { UserMetadata, UserMetadataItem } from 'src/types'; | ||||||
| import { asUuid } from 'src/utils/database'; | import { asUuid } from 'src/utils/database'; | ||||||
|  |  | ||||||
| type Upsert = Insertable<UserMetadataTable>; |  | ||||||
|  |  | ||||||
| export interface UserListFilter { | export interface UserListFilter { | ||||||
|   id?: string; |   id?: string; | ||||||
|   withDeleted?: boolean; |   withDeleted?: boolean; | ||||||
| @@ -211,12 +208,12 @@ export class UserRepository { | |||||||
|   async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { |   async upsertMetadata<T extends keyof UserMetadata>(id: string, { key, value }: { key: T; value: UserMetadata[T] }) { | ||||||
|     await this.db |     await this.db | ||||||
|       .insertInto('user_metadata') |       .insertInto('user_metadata') | ||||||
|       .values({ userId: id, key, value } as Upsert) |       .values({ userId: id, key, value }) | ||||||
|       .onConflict((oc) => |       .onConflict((oc) => | ||||||
|         oc.columns(['userId', 'key']).doUpdateSet({ |         oc.columns(['userId', 'key']).doUpdateSet({ | ||||||
|           key, |           key, | ||||||
|           value, |           value, | ||||||
|         } as Upsert), |         }), | ||||||
|       ) |       ) | ||||||
|       .execute(); |       .execute(); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -230,6 +230,19 @@ export const user_metadata_audit = registerFunction({ | |||||||
|     END`, |     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({ | export const asset_face_audit = registerFunction({ | ||||||
|   name: 'asset_face_audit', |   name: 'asset_face_audit', | ||||||
|   returnType: 'TRIGGER', |   returnType: 'TRIGGER', | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { | |||||||
|   album_user_delete_audit, |   album_user_delete_audit, | ||||||
|   asset_delete_audit, |   asset_delete_audit, | ||||||
|   asset_face_audit, |   asset_face_audit, | ||||||
|  |   asset_metadata_audit, | ||||||
|   f_concat_ws, |   f_concat_ws, | ||||||
|   f_unaccent, |   f_unaccent, | ||||||
|   immich_uuid_v7, |   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 { AssetFaceTable } from 'src/schema/tables/asset-face.table'; | ||||||
| import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | import { AssetFileTable } from 'src/schema/tables/asset-file.table'; | ||||||
| import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.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 { AssetTable } from 'src/schema/tables/asset.table'; | ||||||
| import { AuditTable } from 'src/schema/tables/audit.table'; | import { AuditTable } from 'src/schema/tables/audit.table'; | ||||||
| import { FaceSearchTable } from 'src/schema/tables/face-search.table'; | import { FaceSearchTable } from 'src/schema/tables/face-search.table'; | ||||||
| @@ -81,6 +84,8 @@ export class ImmichDatabase { | |||||||
|     AssetAuditTable, |     AssetAuditTable, | ||||||
|     AssetFaceTable, |     AssetFaceTable, | ||||||
|     AssetFaceAuditTable, |     AssetFaceAuditTable, | ||||||
|  |     AssetMetadataTable, | ||||||
|  |     AssetMetadataAuditTable, | ||||||
|     AssetJobStatusTable, |     AssetJobStatusTable, | ||||||
|     AssetTable, |     AssetTable, | ||||||
|     AssetFileTable, |     AssetFileTable, | ||||||
| @@ -135,6 +140,7 @@ export class ImmichDatabase { | |||||||
|     stack_delete_audit, |     stack_delete_audit, | ||||||
|     person_delete_audit, |     person_delete_audit, | ||||||
|     user_metadata_audit, |     user_metadata_audit, | ||||||
|  |     asset_metadata_audit, | ||||||
|     asset_face_audit, |     asset_face_audit, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
| @@ -164,6 +170,8 @@ export interface DB { | |||||||
|   asset_face: AssetFaceTable; |   asset_face: AssetFaceTable; | ||||||
|   asset_face_audit: AssetFaceAuditTable; |   asset_face_audit: AssetFaceAuditTable; | ||||||
|   asset_file: AssetFileTable; |   asset_file: AssetFileTable; | ||||||
|  |   asset_metadata: AssetMetadataTable; | ||||||
|  |   asset_metadata_audit: AssetMetadataAuditTable; | ||||||
|   asset_job_status: AssetJobStatusTable; |   asset_job_status: AssetJobStatusTable; | ||||||
|   asset_audit: AssetAuditTable; |   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, |       sidecarPath: sidecarFile?.originalPath, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     if (dto.metadata) { | ||||||
|  |       await this.assetRepository.upsertMetadata(asset.id, dto.metadata); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (sidecarFile) { |     if (sidecarFile) { | ||||||
|       await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); |       await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -9,12 +9,14 @@ import { | |||||||
|   AssetBulkUpdateDto, |   AssetBulkUpdateDto, | ||||||
|   AssetJobName, |   AssetJobName, | ||||||
|   AssetJobsDto, |   AssetJobsDto, | ||||||
|  |   AssetMetadataResponseDto, | ||||||
|  |   AssetMetadataUpsertDto, | ||||||
|   AssetStatsDto, |   AssetStatsDto, | ||||||
|   UpdateAssetDto, |   UpdateAssetDto, | ||||||
|   mapStats, |   mapStats, | ||||||
| } from 'src/dtos/asset.dto'; | } from 'src/dtos/asset.dto'; | ||||||
| import { AuthDto } from 'src/dtos/auth.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 { BaseService } from 'src/services/base.service'; | ||||||
| import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; | import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; | ||||||
| import { requireElevatedPermission } from 'src/utils/access'; | 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 }); |     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) { |   async run(auth: AuthDto, dto: AssetJobsDto) { | ||||||
|     await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); |     await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); | ||||||
|  |  | ||||||
| @@ -313,7 +340,7 @@ export class AssetService extends BaseService { | |||||||
|     return asset; |     return asset; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async updateMetadata(dto: ISidecarWriteJob) { |   private async updateExif(dto: ISidecarWriteJob) { | ||||||
|     const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; |     const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; | ||||||
|     const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); |     const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); | ||||||
|     if (Object.keys(writes).length > 0) { |     if (Object.keys(writes).length > 0) { | ||||||
|   | |||||||
| @@ -74,6 +74,7 @@ export const SYNC_TYPES_ORDER = [ | |||||||
|   SyncRequestType.PeopleV1, |   SyncRequestType.PeopleV1, | ||||||
|   SyncRequestType.AssetFacesV1, |   SyncRequestType.AssetFacesV1, | ||||||
|   SyncRequestType.UserMetadataV1, |   SyncRequestType.UserMetadataV1, | ||||||
|  |   SyncRequestType.AssetMetadataV1, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| const throwSessionRequired = () => { | const throwSessionRequired = () => { | ||||||
| @@ -156,6 +157,7 @@ export class SyncService extends BaseService { | |||||||
|       [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), |       [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), | ||||||
|       [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), |       [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), | ||||||
|       [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), |       [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), | ||||||
|  |       [SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth), | ||||||
|       [SyncRequestType.PartnerAssetExifsV1]: () => |       [SyncRequestType.PartnerAssetExifsV1]: () => | ||||||
|         this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id), |         this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id), | ||||||
|       [SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap), |       [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 }) { |   private async upsertBackfillCheckpoint(item: { type: SyncEntityType; sessionId: string; createId: string }) { | ||||||
|     const { type, sessionId, createId } = item; |     const { type, sessionId, createId } = item; | ||||||
|     await this.syncCheckpointRepository.upsertAll([ |     await this.syncCheckpointRepository.upsertAll([ | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { SystemConfig } from 'src/config'; | import { SystemConfig } from 'src/config'; | ||||||
| import { VECTOR_EXTENSIONS } from 'src/constants'; | import { VECTOR_EXTENSIONS } from 'src/constants'; | ||||||
| import { | import { | ||||||
|  |   AssetMetadataKey, | ||||||
|   AssetOrder, |   AssetOrder, | ||||||
|   AssetType, |   AssetType, | ||||||
|   DatabaseSslMode, |   DatabaseSslMode, | ||||||
| @@ -465,11 +466,6 @@ export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, | |||||||
|   [SystemMetadataKey.MemoriesState]: MemoriesState; |   [SystemMetadataKey.MemoriesState]: MemoriesState; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type UserMetadataItem<T extends keyof UserMetadata = UserMetadataKey> = { |  | ||||||
|   key: T; |  | ||||||
|   value: UserMetadata[T]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export interface UserPreferences { | export interface UserPreferences { | ||||||
|   albums: { |   albums: { | ||||||
|     defaultAssetOrder: AssetOrder; |     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>> { | export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> { | ||||||
|   [UserMetadataKey.Preferences]: DeepPartial<UserPreferences>; |   [UserMetadataKey.Preferences]: DeepPartial<UserPreferences>; | ||||||
|   [UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string }; |   [UserMetadataKey.License]: { licenseKey: string; activationKey: string; activatedAt: string }; | ||||||
|   [UserMetadataKey.Onboarding]: { isOnboarded: boolean }; |   [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(), |     filterNewExternalAssetPaths: vitest.fn(), | ||||||
|     updateByLibraryId: vitest.fn(), |     updateByLibraryId: vitest.fn(), | ||||||
|     getFileSamples: vitest.fn(), |     getFileSamples: vitest.fn(), | ||||||
|  |     getMetadata: vitest.fn(), | ||||||
|  |     upsertMetadata: vitest.fn(), | ||||||
|  |     getMetadataByKey: vitest.fn(), | ||||||
|  |     deleteMetadataByKey: vitest.fn(), | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user