mirror of
https://github.com/immich-app/immich.git
synced 2025-11-01 14:37:41 +09:00
feat: asset metadata (#20446)
This commit is contained in:
10
mobile/openapi/README.md
generated
10
mobile/openapi/README.md
generated
@@ -97,16 +97,20 @@ Class | Method | HTTP request | Description
|
|||||||
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
|
*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