mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(server)!: add/remove album assets (#3109)
* refactor: add/remove album assets * chore: open api * feat: remove owned assets from album * refactor: move to bulk id req/res dto * chore: open api * chore: merge main * dev: mobile work * fix: adding asset from web not sync with mobile * remove print statement --------- Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
		
							
								
								
									
										116
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										116
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -99,44 +99,6 @@ export interface APIKeyUpdateDto { | ||||
|      */ | ||||
|     'name': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AddAssetsDto | ||||
|  */ | ||||
| export interface AddAssetsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AddAssetsDto | ||||
|      */ | ||||
|     'assetIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AddAssetsResponseDto | ||||
|  */ | ||||
| export interface AddAssetsResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AlbumResponseDto} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'album'?: AlbumResponseDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'alreadyInAlbum': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'successfullyAdded': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -821,6 +783,19 @@ export const BulkIdResponseDtoErrorEnum = { | ||||
| 
 | ||||
| export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum]; | ||||
| 
 | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface BulkIdsDto | ||||
|  */ | ||||
| export interface BulkIdsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof BulkIdsDto | ||||
|      */ | ||||
|     'ids': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -1927,19 +1902,6 @@ export interface QueueStatusDto { | ||||
|      */ | ||||
|     'isPaused': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface RemoveAssetsDto | ||||
|  */ | ||||
| export interface RemoveAssetsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof RemoveAssetsDto | ||||
|      */ | ||||
|     'assetIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -3678,16 +3640,16 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToAlbum: async (id: string, addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         addAssetsToAlbum: async (id: string, bulkIdsDto: BulkIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'id' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'id', id) | ||||
|             // verify required parameter 'addAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'addAssetsDto', addAssetsDto) | ||||
|             // verify required parameter 'bulkIdsDto' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'bulkIdsDto', bulkIdsDto) | ||||
|             const localVarPath = `/album/{id}/assets` | ||||
|                 .replace(`{${"id"}}`, encodeURIComponent(String(id))); | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
| @@ -3721,7 +3683,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration) | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
| @@ -3998,15 +3960,15 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetFromAlbum: async (id: string, removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         removeAssetFromAlbum: async (id: string, bulkIdsDto: BulkIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'id' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'id', id) | ||||
|             // verify required parameter 'removeAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'removeAssetsDto', removeAssetsDto) | ||||
|             // verify required parameter 'bulkIdsDto' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'bulkIdsDto', bulkIdsDto) | ||||
|             const localVarPath = `/album/{id}/assets` | ||||
|                 .replace(`{${"id"}}`, encodeURIComponent(String(id))); | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
| @@ -4036,7 +3998,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration) | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
| @@ -4150,13 +4112,13 @@ export const AlbumApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AddAssetsResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, addAssetsDto, key, options); | ||||
|         async addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, bulkIdsDto, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -4224,12 +4186,12 @@ export const AlbumApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, removeAssetsDto, options); | ||||
|         async removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, bulkIdsDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -4270,8 +4232,8 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<AddAssetsResponseDto> { | ||||
|             return localVarFp.addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|         addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> { | ||||
|             return localVarFp.addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -4332,8 +4294,8 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<AlbumResponseDto> { | ||||
|             return localVarFp.removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(axios, basePath)); | ||||
|         removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> { | ||||
|             return localVarFp.removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -4371,10 +4333,10 @@ export interface AlbumApiAddAssetsToAlbumRequest { | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AddAssetsDto} | ||||
|      * @type {BulkIdsDto} | ||||
|      * @memberof AlbumApiAddAssetsToAlbum | ||||
|      */ | ||||
|     readonly addAssetsDto: AddAssetsDto | ||||
|     readonly bulkIdsDto: BulkIdsDto | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
| @@ -4490,10 +4452,10 @@ export interface AlbumApiRemoveAssetFromAlbumRequest { | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {RemoveAssetsDto} | ||||
|      * @type {BulkIdsDto} | ||||
|      * @memberof AlbumApiRemoveAssetFromAlbum | ||||
|      */ | ||||
|     readonly removeAssetsDto: RemoveAssetsDto | ||||
|     readonly bulkIdsDto: BulkIdsDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @@ -4553,7 +4515,7 @@ export class AlbumApi extends BaseAPI { | ||||
|      * @memberof AlbumApi | ||||
|      */ | ||||
|     public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -4629,7 +4591,7 @@ export class AlbumApi extends BaseAPI { | ||||
|      * @memberof AlbumApi | ||||
|      */ | ||||
|     public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -0,0 +1,49 @@ | ||||
| // ignore_for_file: public_member_api_docs, sort_constructors_first | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
|  | ||||
| class AddAssetsResponse { | ||||
|   List<String> alreadyInAlbum; | ||||
|   int successfullyAdded; | ||||
|  | ||||
|   AddAssetsResponse({ | ||||
|     required this.alreadyInAlbum, | ||||
|     required this.successfullyAdded, | ||||
|   }); | ||||
|  | ||||
|   AddAssetsResponse copyWith({ | ||||
|     List<String>? alreadyInAlbum, | ||||
|     int? successfullyAdded, | ||||
|   }) { | ||||
|     return AddAssetsResponse( | ||||
|       alreadyInAlbum: alreadyInAlbum ?? this.alreadyInAlbum, | ||||
|       successfullyAdded: successfullyAdded ?? this.successfullyAdded, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Map<String, dynamic> toMap() { | ||||
|     return <String, dynamic>{ | ||||
|       'alreadyInAlbum': alreadyInAlbum, | ||||
|       'successfullyAdded': successfullyAdded, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   String toJson() => json.encode(toMap()); | ||||
|  | ||||
|   @override | ||||
|   String toString() => | ||||
|       'AddAssetsResponse(alreadyInAlbum: $alreadyInAlbum, successfullyAdded: $successfullyAdded)'; | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(covariant AddAssetsResponse other) { | ||||
|     if (identical(this, other)) return true; | ||||
|     final listEquals = const DeepCollectionEquality().equals; | ||||
|  | ||||
|     return listEquals(other.alreadyInAlbum, alreadyInAlbum) && | ||||
|         other.successfullyAdded == successfullyAdded; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => alreadyInAlbum.hashCode ^ successfullyAdded.hashCode; | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
|  | ||||
| final albumDetailProvider = | ||||
|     StreamProvider.family<Album, int>((ref, albumId) async* { | ||||
|   final user = ref.watch(currentUserProvider); | ||||
|   if (user == null) return; | ||||
|   final AlbumService service = ref.watch(albumServiceProvider); | ||||
|  | ||||
|   await for (final a in service.watchAlbum(albumId)) { | ||||
|     if (a == null) { | ||||
|       throw Exception("Album with ID=$albumId does not exist anymore!"); | ||||
|     } | ||||
|     await for (final _ in a.watchRenderList(GroupAssetsBy.none)) { | ||||
|       yield a; | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| @@ -3,12 +3,10 @@ import 'dart:async'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| import 'package:immich_mobile/shared/models/asset.dart'; | ||||
| import 'package:immich_mobile/shared/models/user.dart'; | ||||
| import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||
| import 'package:immich_mobile/shared/providers/user.provider.dart'; | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| class SharedAlbumNotifier extends StateNotifier<List<Album>> { | ||||
| @@ -72,19 +70,3 @@ final sharedAlbumProvider = | ||||
|     ref.watch(dbProvider), | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| final sharedAlbumDetailProvider = | ||||
|     StreamProvider.family<Album, int>((ref, albumId) async* { | ||||
|   final user = ref.watch(currentUserProvider); | ||||
|   if (user == null) return; | ||||
|   final AlbumService sharedAlbumService = ref.watch(albumServiceProvider); | ||||
|  | ||||
|   await for (final a in sharedAlbumService.watchAlbum(albumId)) { | ||||
|     if (a == null) { | ||||
|       throw Exception("Album with ID=$albumId does not exist anymore!"); | ||||
|     } | ||||
|     await for (final _ in a.watchRenderList(GroupAssetsBy.none)) { | ||||
|       yield a; | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'dart:io'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/models/add_asset_response.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/models/backup_album.model.dart'; | ||||
| import 'package:immich_mobile/modules/backup/services/backup.service.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| @@ -219,24 +220,43 @@ class AlbumService { | ||||
|     yield* _db.albums.watchObject(albumId); | ||||
|   } | ||||
|  | ||||
|   Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum( | ||||
|   Future<AddAssetsResponse?> addAdditionalAssetToAlbum( | ||||
|     Iterable<Asset> assets, | ||||
|     Album album, | ||||
|   ) async { | ||||
|     try { | ||||
|       var result = await _apiService.albumApi.addAssetsToAlbum( | ||||
|       var response = await _apiService.albumApi.addAssetsToAlbum( | ||||
|         album.remoteId!, | ||||
|         AddAssetsDto(assetIds: assets.map((asset) => asset.remoteId!).toList()), | ||||
|         BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()), | ||||
|       ); | ||||
|  | ||||
|       if (response != null) { | ||||
|         List<Asset> successAssets = []; | ||||
|         List<String> duplicatedAssets = []; | ||||
|  | ||||
|         for (final result in response) { | ||||
|           if (result.success) { | ||||
|             successAssets | ||||
|                 .add(assets.firstWhere((asset) => asset.remoteId == result.id)); | ||||
|           } else if (!result.success && | ||||
|               result.error == BulkIdResponseDtoErrorEnum.duplicate) { | ||||
|             duplicatedAssets.add(result.id); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         album.assets.addAll(successAssets); | ||||
|         await _db.writeTxn(() => album.assets.save()); | ||||
|  | ||||
|         return AddAssetsResponse( | ||||
|           alreadyInAlbum: duplicatedAssets, | ||||
|           successfullyAdded: successAssets.length, | ||||
|         ); | ||||
|       if (result != null && result.successfullyAdded > 0) { | ||||
|         album.assets.addAll(assets); | ||||
|         await _db.writeTxn(() => album.assets.save()); | ||||
|       } | ||||
|       return result; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error addAdditionalAssetToAlbum  ${e.toString()}"); | ||||
|       return null; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<bool> addAdditionalUserToAlbum( | ||||
| @@ -314,8 +334,8 @@ class AlbumService { | ||||
|     try { | ||||
|       await _apiService.albumApi.removeAssetFromAlbum( | ||||
|         album.remoteId!, | ||||
|         RemoveAssetsDto( | ||||
|           assetIds: assets.map((e) => e.remoteId!).toList(growable: false), | ||||
|         BulkIdsDto( | ||||
|           ids: assets.map((asset) => asset.remoteId!).toList(), | ||||
|         ), | ||||
|       ); | ||||
|       album.assets.removeAll(assets); | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart'; | ||||
| @@ -63,9 +64,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ref.read(albumProvider.notifier).getAllAlbums(); | ||||
|       ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); | ||||
|  | ||||
|       ref.invalidate(albumDetailProvider(album.id)); | ||||
|       Navigator.pop(context); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| @@ -99,7 +100,7 @@ class AlbumViewerAppbar extends HookConsumerWidget | ||||
|         Navigator.pop(context); | ||||
|         selectionDisabled(); | ||||
|         ref.watch(albumProvider.notifier).getAllAlbums(); | ||||
|         ref.invalidate(sharedAlbumDetailProvider(album.id)); | ||||
|         ref.invalidate(albumDetailProvider(album.id)); | ||||
|       } else { | ||||
|         Navigator.pop(context); | ||||
|         ImmichToast.show( | ||||
|   | ||||
| @@ -6,13 +6,12 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart'; | ||||
| import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; | ||||
| import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
| import 'package:immich_mobile/shared/models/album.dart'; | ||||
| @@ -28,11 +27,20 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     FocusNode titleFocusNode = useFocusNode(); | ||||
|     final album = ref.watch(sharedAlbumDetailProvider(albumId)); | ||||
|     final album = ref.watch(albumDetailProvider(albumId)); | ||||
|     final userId = ref.watch(authenticationProvider).userId; | ||||
|     final selection = useState<Set<Asset>>({}); | ||||
|     final multiSelectEnabled = useState(false); | ||||
|  | ||||
|     useEffect( | ||||
|       () { | ||||
|         // Fetch album updates, e.g., cover image | ||||
|         ref.invalidate(albumDetailProvider(albumId)); | ||||
|         return null; | ||||
|       }, | ||||
|       [], | ||||
|     ); | ||||
|  | ||||
|     Future<bool> onWillPop() async { | ||||
|       if (multiSelectEnabled.value) { | ||||
|         selection.value = {}; | ||||
| @@ -77,8 +85,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|  | ||||
|           if (addAssetsResult != null && | ||||
|               addAssetsResult.successfullyAdded > 0) { | ||||
|             ref.watch(albumProvider.notifier).getAllAlbums(); | ||||
|             ref.invalidate(sharedAlbumDetailProvider(albumId)); | ||||
|             ref.invalidate(albumDetailProvider(albumId)); | ||||
|           } | ||||
|  | ||||
|           ImmichLoadingOverlayController.appLoader.hide(); | ||||
| @@ -100,7 +107,7 @@ class AlbumViewerPage extends HookConsumerWidget { | ||||
|             .addAdditionalUserToAlbum(sharedUserIds, album); | ||||
|  | ||||
|         if (isSuccess) { | ||||
|           ref.invalidate(sharedAlbumDetailProvider(album.id)); | ||||
|           ref.invalidate(albumDetailProvider(album.id)); | ||||
|         } | ||||
|  | ||||
|         ImmichLoadingOverlayController.appLoader.hide(); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:fluttertoast/fluttertoast.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; | ||||
| import 'package:immich_mobile/modules/album/services/album.service.dart'; | ||||
| import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; | ||||
| @@ -208,6 +209,9 @@ class HomePage extends HookConsumerWidget { | ||||
|                 ), | ||||
|                 toastType: ToastType.success, | ||||
|               ); | ||||
|  | ||||
|               ref.watch(albumProvider.notifier).getAllAlbums(); | ||||
|               ref.invalidate(albumDetailProvider(album.id)); | ||||
|             } | ||||
|           } | ||||
|         } finally { | ||||
|   | ||||
							
								
								
									
										12
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -8,8 +8,6 @@ doc/APIKeyCreateDto.md | ||||
| doc/APIKeyCreateResponseDto.md | ||||
| doc/APIKeyResponseDto.md | ||||
| doc/APIKeyUpdateDto.md | ||||
| doc/AddAssetsDto.md | ||||
| doc/AddAssetsResponseDto.md | ||||
| doc/AddUsersDto.md | ||||
| doc/AdminSignupResponseDto.md | ||||
| doc/AlbumApi.md | ||||
| @@ -33,6 +31,7 @@ doc/AudioCodec.md | ||||
| doc/AuthDeviceResponseDto.md | ||||
| doc/AuthenticationApi.md | ||||
| doc/BulkIdResponseDto.md | ||||
| doc/BulkIdsDto.md | ||||
| doc/ChangePasswordDto.md | ||||
| doc/CheckDuplicateAssetDto.md | ||||
| doc/CheckDuplicateAssetResponseDto.md | ||||
| @@ -78,7 +77,6 @@ doc/PersonApi.md | ||||
| doc/PersonResponseDto.md | ||||
| doc/PersonUpdateDto.md | ||||
| doc/QueueStatusDto.md | ||||
| doc/RemoveAssetsDto.md | ||||
| doc/SearchAlbumResponseDto.md | ||||
| doc/SearchApi.md | ||||
| doc/SearchAssetDto.md | ||||
| @@ -150,8 +148,6 @@ lib/auth/authentication.dart | ||||
| lib/auth/http_basic_auth.dart | ||||
| lib/auth/http_bearer_auth.dart | ||||
| lib/auth/oauth.dart | ||||
| lib/model/add_assets_dto.dart | ||||
| lib/model/add_assets_response_dto.dart | ||||
| lib/model/add_users_dto.dart | ||||
| lib/model/admin_signup_response_dto.dart | ||||
| lib/model/album_count_response_dto.dart | ||||
| @@ -176,6 +172,7 @@ lib/model/asset_type_enum.dart | ||||
| lib/model/audio_codec.dart | ||||
| lib/model/auth_device_response_dto.dart | ||||
| lib/model/bulk_id_response_dto.dart | ||||
| lib/model/bulk_ids_dto.dart | ||||
| lib/model/change_password_dto.dart | ||||
| lib/model/check_duplicate_asset_dto.dart | ||||
| lib/model/check_duplicate_asset_response_dto.dart | ||||
| @@ -217,7 +214,6 @@ lib/model/people_update_item.dart | ||||
| lib/model/person_response_dto.dart | ||||
| lib/model/person_update_dto.dart | ||||
| lib/model/queue_status_dto.dart | ||||
| lib/model/remove_assets_dto.dart | ||||
| lib/model/search_album_response_dto.dart | ||||
| lib/model/search_asset_dto.dart | ||||
| lib/model/search_asset_response_dto.dart | ||||
| @@ -260,8 +256,6 @@ lib/model/user_response_dto.dart | ||||
| lib/model/validate_access_token_response_dto.dart | ||||
| lib/model/video_codec.dart | ||||
| pubspec.yaml | ||||
| test/add_assets_dto_test.dart | ||||
| test/add_assets_response_dto_test.dart | ||||
| test/add_users_dto_test.dart | ||||
| test/admin_signup_response_dto_test.dart | ||||
| test/album_api_test.dart | ||||
| @@ -290,6 +284,7 @@ test/audio_codec_test.dart | ||||
| test/auth_device_response_dto_test.dart | ||||
| test/authentication_api_test.dart | ||||
| test/bulk_id_response_dto_test.dart | ||||
| test/bulk_ids_dto_test.dart | ||||
| test/change_password_dto_test.dart | ||||
| test/check_duplicate_asset_dto_test.dart | ||||
| test/check_duplicate_asset_response_dto_test.dart | ||||
| @@ -335,7 +330,6 @@ test/person_api_test.dart | ||||
| test/person_response_dto_test.dart | ||||
| test/person_update_dto_test.dart | ||||
| test/queue_status_dto_test.dart | ||||
| test/remove_assets_dto_test.dart | ||||
| test/search_album_response_dto_test.dart | ||||
| test/search_api_test.dart | ||||
| test/search_asset_dto_test.dart | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -182,8 +182,6 @@ Class | Method | HTTP request | Description | ||||
|  - [APIKeyCreateResponseDto](doc//APIKeyCreateResponseDto.md) | ||||
|  - [APIKeyResponseDto](doc//APIKeyResponseDto.md) | ||||
|  - [APIKeyUpdateDto](doc//APIKeyUpdateDto.md) | ||||
|  - [AddAssetsDto](doc//AddAssetsDto.md) | ||||
|  - [AddAssetsResponseDto](doc//AddAssetsResponseDto.md) | ||||
|  - [AddUsersDto](doc//AddUsersDto.md) | ||||
|  - [AdminSignupResponseDto](doc//AdminSignupResponseDto.md) | ||||
|  - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) | ||||
| @@ -204,6 +202,7 @@ Class | Method | HTTP request | Description | ||||
|  - [AudioCodec](doc//AudioCodec.md) | ||||
|  - [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md) | ||||
|  - [BulkIdResponseDto](doc//BulkIdResponseDto.md) | ||||
|  - [BulkIdsDto](doc//BulkIdsDto.md) | ||||
|  - [ChangePasswordDto](doc//ChangePasswordDto.md) | ||||
|  - [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md) | ||||
|  - [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md) | ||||
| @@ -245,7 +244,6 @@ Class | Method | HTTP request | Description | ||||
|  - [PersonResponseDto](doc//PersonResponseDto.md) | ||||
|  - [PersonUpdateDto](doc//PersonUpdateDto.md) | ||||
|  - [QueueStatusDto](doc//QueueStatusDto.md) | ||||
|  - [RemoveAssetsDto](doc//RemoveAssetsDto.md) | ||||
|  - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) | ||||
|  - [SearchAssetDto](doc//SearchAssetDto.md) | ||||
|  - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) | ||||
|   | ||||
							
								
								
									
										17
									
								
								mobile/openapi/doc/AddAssetsResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								mobile/openapi/doc/AddAssetsResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -1,17 +0,0 @@ | ||||
| # openapi.model.AddAssetsResponseDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **album** | [**AlbumResponseDto**](AlbumResponseDto.md) |  | [optional]  | ||||
| **alreadyInAlbum** | **List<String>** |  | [default to const []] | ||||
| **successfullyAdded** | **int** |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										20
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								mobile/openapi/doc/AlbumApi.md
									
									
									
										generated
									
									
									
								
							| @@ -22,7 +22,7 @@ Method | HTTP request | Description | ||||
| 
 | ||||
| 
 | ||||
| # **addAssetsToAlbum** | ||||
| > AddAssetsResponseDto addAssetsToAlbum(id, addAssetsDto, key) | ||||
| > List<BulkIdResponseDto> addAssetsToAlbum(id, bulkIdsDto, key) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -46,11 +46,11 @@ import 'package:openapi/api.dart'; | ||||
| 
 | ||||
| final api_instance = AlbumApi(); | ||||
| final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final addAssetsDto = AddAssetsDto(); // AddAssetsDto |  | ||||
| final bulkIdsDto = BulkIdsDto(); // BulkIdsDto |  | ||||
| final key = key_example; // String |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.addAssetsToAlbum(id, addAssetsDto, key); | ||||
|     final result = api_instance.addAssetsToAlbum(id, bulkIdsDto, key); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AlbumApi->addAssetsToAlbum: $e\n'); | ||||
| @@ -62,12 +62,12 @@ try { | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **id** | **String**|  |  | ||||
|  **addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.md)|  |  | ||||
|  **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)|  |  | ||||
|  **key** | **String**|  | [optional]  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**AddAssetsResponseDto**](AddAssetsResponseDto.md) | ||||
| [**List<BulkIdResponseDto>**](BulkIdResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| @@ -412,7 +412,7 @@ Name | Type | Description  | Notes | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **removeAssetFromAlbum** | ||||
| > AlbumResponseDto removeAssetFromAlbum(id, removeAssetsDto) | ||||
| > List<BulkIdResponseDto> removeAssetFromAlbum(id, bulkIdsDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -436,10 +436,10 @@ import 'package:openapi/api.dart'; | ||||
| 
 | ||||
| final api_instance = AlbumApi(); | ||||
| final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |  | ||||
| final bulkIdsDto = BulkIdsDto(); // BulkIdsDto |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.removeAssetFromAlbum(id, removeAssetsDto); | ||||
|     final result = api_instance.removeAssetFromAlbum(id, bulkIdsDto); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AlbumApi->removeAssetFromAlbum: $e\n'); | ||||
| @@ -451,11 +451,11 @@ try { | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **id** | **String**|  |  | ||||
|  **removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.md)|  |  | ||||
|  **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**AlbumResponseDto**](AlbumResponseDto.md) | ||||
| [**List<BulkIdResponseDto>**](BulkIdResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # openapi.model.AddAssetsDto | ||||
| # openapi.model.BulkIdsDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **assetIds** | **List<String>** |  | [default to const []] | ||||
| **ids** | **List<String>** |  | [default to const []] | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
							
								
								
									
										15
									
								
								mobile/openapi/doc/RemoveAssetsDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								mobile/openapi/doc/RemoveAssetsDto.md
									
									
									
										generated
									
									
									
								
							| @@ -1,15 +0,0 @@ | ||||
| # openapi.model.RemoveAssetsDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **assetIds** | **List<String>** |  | [default to const []] | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										4
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -47,8 +47,6 @@ part 'model/api_key_create_dto.dart'; | ||||
| part 'model/api_key_create_response_dto.dart'; | ||||
| part 'model/api_key_response_dto.dart'; | ||||
| part 'model/api_key_update_dto.dart'; | ||||
| part 'model/add_assets_dto.dart'; | ||||
| part 'model/add_assets_response_dto.dart'; | ||||
| part 'model/add_users_dto.dart'; | ||||
| part 'model/admin_signup_response_dto.dart'; | ||||
| part 'model/album_count_response_dto.dart'; | ||||
| @@ -69,6 +67,7 @@ part 'model/asset_type_enum.dart'; | ||||
| part 'model/audio_codec.dart'; | ||||
| part 'model/auth_device_response_dto.dart'; | ||||
| part 'model/bulk_id_response_dto.dart'; | ||||
| part 'model/bulk_ids_dto.dart'; | ||||
| part 'model/change_password_dto.dart'; | ||||
| part 'model/check_duplicate_asset_dto.dart'; | ||||
| part 'model/check_duplicate_asset_response_dto.dart'; | ||||
| @@ -110,7 +109,6 @@ part 'model/people_update_item.dart'; | ||||
| part 'model/person_response_dto.dart'; | ||||
| part 'model/person_update_dto.dart'; | ||||
| part 'model/queue_status_dto.dart'; | ||||
| part 'model/remove_assets_dto.dart'; | ||||
| part 'model/search_album_response_dto.dart'; | ||||
| part 'model/search_asset_dto.dart'; | ||||
| part 'model/search_asset_response_dto.dart'; | ||||
|   | ||||
							
								
								
									
										34
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										34
									
								
								mobile/openapi/lib/api/album_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -21,16 +21,16 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AddAssetsDto] addAssetsDto (required): | ||||
|   /// * [BulkIdsDto] bulkIdsDto (required): | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<Response> addAssetsToAlbumWithHttpInfo(String id, AddAssetsDto addAssetsDto, { String? key, }) async { | ||||
|   Future<Response> addAssetsToAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto, { String? key, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/album/{id}/assets' | ||||
|       .replaceAll('{id}', id); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = addAssetsDto; | ||||
|     Object? postBody = bulkIdsDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
| @@ -58,11 +58,11 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [AddAssetsDto] addAssetsDto (required): | ||||
|   /// * [BulkIdsDto] bulkIdsDto (required): | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<AddAssetsResponseDto?> addAssetsToAlbum(String id, AddAssetsDto addAssetsDto, { String? key, }) async { | ||||
|     final response = await addAssetsToAlbumWithHttpInfo(id, addAssetsDto,  key: key, ); | ||||
|   Future<List<BulkIdResponseDto>?> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String? key, }) async { | ||||
|     final response = await addAssetsToAlbumWithHttpInfo(id, bulkIdsDto,  key: key, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -70,7 +70,10 @@ class AlbumApi { | ||||
|     // 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), 'AddAssetsResponseDto',) as AddAssetsResponseDto; | ||||
|       final responseBody = await _decodeBodyBytes(response); | ||||
|       return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List) | ||||
|         .cast<BulkIdResponseDto>() | ||||
|         .toList(); | ||||
| 
 | ||||
|     } | ||||
|     return null; | ||||
| @@ -380,14 +383,14 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [RemoveAssetsDto] removeAssetsDto (required): | ||||
|   Future<Response> removeAssetFromAlbumWithHttpInfo(String id, RemoveAssetsDto removeAssetsDto,) async { | ||||
|   /// * [BulkIdsDto] bulkIdsDto (required): | ||||
|   Future<Response> removeAssetFromAlbumWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/album/{id}/assets' | ||||
|       .replaceAll('{id}', id); | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = removeAssetsDto; | ||||
|     Object? postBody = bulkIdsDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
| @@ -411,9 +414,9 @@ class AlbumApi { | ||||
|   /// | ||||
|   /// * [String] id (required): | ||||
|   /// | ||||
|   /// * [RemoveAssetsDto] removeAssetsDto (required): | ||||
|   Future<AlbumResponseDto?> removeAssetFromAlbum(String id, RemoveAssetsDto removeAssetsDto,) async { | ||||
|     final response = await removeAssetFromAlbumWithHttpInfo(id, removeAssetsDto,); | ||||
|   /// * [BulkIdsDto] bulkIdsDto (required): | ||||
|   Future<List<BulkIdResponseDto>?> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto,) async { | ||||
|     final response = await removeAssetFromAlbumWithHttpInfo(id, bulkIdsDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -421,7 +424,10 @@ class AlbumApi { | ||||
|     // 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), 'AlbumResponseDto',) as AlbumResponseDto; | ||||
|       final responseBody = await _decodeBodyBytes(response); | ||||
|       return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List) | ||||
|         .cast<BulkIdResponseDto>() | ||||
|         .toList(); | ||||
| 
 | ||||
|     } | ||||
|     return null; | ||||
|   | ||||
							
								
								
									
										8
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -189,10 +189,6 @@ class ApiClient { | ||||
|           return APIKeyResponseDto.fromJson(value); | ||||
|         case 'APIKeyUpdateDto': | ||||
|           return APIKeyUpdateDto.fromJson(value); | ||||
|         case 'AddAssetsDto': | ||||
|           return AddAssetsDto.fromJson(value); | ||||
|         case 'AddAssetsResponseDto': | ||||
|           return AddAssetsResponseDto.fromJson(value); | ||||
|         case 'AddUsersDto': | ||||
|           return AddUsersDto.fromJson(value); | ||||
|         case 'AdminSignupResponseDto': | ||||
| @@ -233,6 +229,8 @@ class ApiClient { | ||||
|           return AuthDeviceResponseDto.fromJson(value); | ||||
|         case 'BulkIdResponseDto': | ||||
|           return BulkIdResponseDto.fromJson(value); | ||||
|         case 'BulkIdsDto': | ||||
|           return BulkIdsDto.fromJson(value); | ||||
|         case 'ChangePasswordDto': | ||||
|           return ChangePasswordDto.fromJson(value); | ||||
|         case 'CheckDuplicateAssetDto': | ||||
| @@ -315,8 +313,6 @@ class ApiClient { | ||||
|           return PersonUpdateDto.fromJson(value); | ||||
|         case 'QueueStatusDto': | ||||
|           return QueueStatusDto.fromJson(value); | ||||
|         case 'RemoveAssetsDto': | ||||
|           return RemoveAssetsDto.fromJson(value); | ||||
|         case 'SearchAlbumResponseDto': | ||||
|           return SearchAlbumResponseDto.fromJson(value); | ||||
|         case 'SearchAssetDto': | ||||
|   | ||||
							
								
								
									
										125
									
								
								mobile/openapi/lib/model/add_assets_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										125
									
								
								mobile/openapi/lib/model/add_assets_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -1,125 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // 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 AddAssetsResponseDto { | ||||
|   /// Returns a new [AddAssetsResponseDto] instance. | ||||
|   AddAssetsResponseDto({ | ||||
|     this.album, | ||||
|     this.alreadyInAlbum = const [], | ||||
|     required this.successfullyAdded, | ||||
|   }); | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   AlbumResponseDto? album; | ||||
| 
 | ||||
|   List<String> alreadyInAlbum; | ||||
| 
 | ||||
|   int successfullyAdded; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AddAssetsResponseDto && | ||||
|      other.album == album && | ||||
|      other.alreadyInAlbum == alreadyInAlbum && | ||||
|      other.successfullyAdded == successfullyAdded; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (album == null ? 0 : album!.hashCode) + | ||||
|     (alreadyInAlbum.hashCode) + | ||||
|     (successfullyAdded.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AddAssetsResponseDto[album=$album, alreadyInAlbum=$alreadyInAlbum, successfullyAdded=$successfullyAdded]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|     if (this.album != null) { | ||||
|       json[r'album'] = this.album; | ||||
|     } else { | ||||
|     //  json[r'album'] = null; | ||||
|     } | ||||
|       json[r'alreadyInAlbum'] = this.alreadyInAlbum; | ||||
|       json[r'successfullyAdded'] = this.successfullyAdded; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AddAssetsResponseDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AddAssetsResponseDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AddAssetsResponseDto( | ||||
|         album: AlbumResponseDto.fromJson(json[r'album']), | ||||
|         alreadyInAlbum: json[r'alreadyInAlbum'] is Iterable | ||||
|             ? (json[r'alreadyInAlbum'] as Iterable).cast<String>().toList(growable: false) | ||||
|             : const [], | ||||
|         successfullyAdded: mapValueOfType<int>(json, r'successfullyAdded')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AddAssetsResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AddAssetsResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AddAssetsResponseDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AddAssetsResponseDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AddAssetsResponseDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AddAssetsResponseDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AddAssetsResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<AddAssetsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AddAssetsResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AddAssetsResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'alreadyInAlbum', | ||||
|     'successfullyAdded', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| @@ -10,53 +10,53 @@ | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class AddAssetsDto { | ||||
|   /// Returns a new [AddAssetsDto] instance. | ||||
|   AddAssetsDto({ | ||||
|     this.assetIds = const [], | ||||
| class BulkIdsDto { | ||||
|   /// Returns a new [BulkIdsDto] instance. | ||||
|   BulkIdsDto({ | ||||
|     this.ids = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<String> assetIds; | ||||
|   List<String> ids; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AddAssetsDto && | ||||
|      other.assetIds == assetIds; | ||||
|   bool operator ==(Object other) => identical(this, other) || other is BulkIdsDto && | ||||
|      other.ids == ids; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (assetIds.hashCode); | ||||
|     (ids.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AddAssetsDto[assetIds=$assetIds]'; | ||||
|   String toString() => 'BulkIdsDto[ids=$ids]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'assetIds'] = this.assetIds; | ||||
|       json[r'ids'] = this.ids; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AddAssetsDto] instance and imports its values from | ||||
|   /// Returns a new [BulkIdsDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AddAssetsDto? fromJson(dynamic value) { | ||||
|   static BulkIdsDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AddAssetsDto( | ||||
|         assetIds: json[r'assetIds'] is Iterable | ||||
|             ? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false) | ||||
|       return BulkIdsDto( | ||||
|         ids: json[r'ids'] is Iterable | ||||
|             ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false) | ||||
|             : const [], | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AddAssetsDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AddAssetsDto>[]; | ||||
|   static List<BulkIdsDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <BulkIdsDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AddAssetsDto.fromJson(row); | ||||
|         final value = BulkIdsDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
| @@ -65,12 +65,12 @@ class AddAssetsDto { | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AddAssetsDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AddAssetsDto>{}; | ||||
|   static Map<String, BulkIdsDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, BulkIdsDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AddAssetsDto.fromJson(entry.value); | ||||
|         final value = BulkIdsDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
| @@ -79,14 +79,14 @@ class AddAssetsDto { | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AddAssetsDto-objects as value to a dart map | ||||
|   static Map<String, List<AddAssetsDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AddAssetsDto>>{}; | ||||
|   // maps a json object with a list of BulkIdsDto-objects as value to a dart map | ||||
|   static Map<String, List<BulkIdsDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<BulkIdsDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AddAssetsDto.listFromJson(entry.value, growable: growable,); | ||||
|         map[entry.key] = BulkIdsDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
| @@ -94,7 +94,7 @@ class AddAssetsDto { | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'assetIds', | ||||
|     'ids', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										100
									
								
								mobile/openapi/lib/model/remove_assets_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										100
									
								
								mobile/openapi/lib/model/remove_assets_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -1,100 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // 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 RemoveAssetsDto { | ||||
|   /// Returns a new [RemoveAssetsDto] instance. | ||||
|   RemoveAssetsDto({ | ||||
|     this.assetIds = const [], | ||||
|   }); | ||||
| 
 | ||||
|   List<String> assetIds; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is RemoveAssetsDto && | ||||
|      other.assetIds == assetIds; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (assetIds.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'RemoveAssetsDto[assetIds=$assetIds]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'assetIds'] = this.assetIds; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [RemoveAssetsDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static RemoveAssetsDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return RemoveAssetsDto( | ||||
|         assetIds: json[r'assetIds'] is Iterable | ||||
|             ? (json[r'assetIds'] as Iterable).cast<String>().toList(growable: false) | ||||
|             : const [], | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<RemoveAssetsDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <RemoveAssetsDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = RemoveAssetsDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, RemoveAssetsDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, RemoveAssetsDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = RemoveAssetsDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of RemoveAssetsDto-objects as value to a dart map | ||||
|   static Map<String, List<RemoveAssetsDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<RemoveAssetsDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = RemoveAssetsDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'assetIds', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| @@ -1,37 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // 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 | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for AddAssetsResponseDto | ||||
| void main() { | ||||
|   // final instance = AddAssetsResponseDto(); | ||||
| 
 | ||||
|   group('test AddAssetsResponseDto', () { | ||||
|     // AlbumResponseDto album | ||||
|     test('to test the property `album`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<String> alreadyInAlbum (default value: const []) | ||||
|     test('to test the property `alreadyInAlbum`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // int successfullyAdded | ||||
|     test('to test the property `successfullyAdded`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										4
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/test/album_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -17,7 +17,7 @@ void main() { | ||||
|   // final instance = AlbumApi(); | ||||
| 
 | ||||
|   group('tests for AlbumApi', () { | ||||
|     //Future<AddAssetsResponseDto> addAssetsToAlbum(String id, AddAssetsDto addAssetsDto, { String key }) async | ||||
|     //Future<List<BulkIdResponseDto>> addAssetsToAlbum(String id, BulkIdsDto bulkIdsDto, { String key }) async | ||||
|     test('test addAssetsToAlbum', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| @@ -52,7 +52,7 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<AlbumResponseDto> removeAssetFromAlbum(String id, RemoveAssetsDto removeAssetsDto) async | ||||
|     //Future<List<BulkIdResponseDto>> removeAssetFromAlbum(String id, BulkIdsDto bulkIdsDto) async | ||||
|     test('test removeAssetFromAlbum', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -11,13 +11,13 @@ | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for AddAssetsDto | ||||
| // tests for BulkIdsDto | ||||
| void main() { | ||||
|   // final instance = AddAssetsDto(); | ||||
|   // final instance = BulkIdsDto(); | ||||
| 
 | ||||
|   group('test AddAssetsDto', () { | ||||
|     // List<String> assetIds (default value: const []) | ||||
|     test('to test the property `assetIds`', () async { | ||||
|   group('test BulkIdsDto', () { | ||||
|     // List<String> ids (default value: const []) | ||||
|     test('to test the property `ids`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
							
								
								
									
										27
									
								
								mobile/openapi/test/remove_assets_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								mobile/openapi/test/remove_assets_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -1,27 +0,0 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // 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 | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for RemoveAssetsDto | ||||
| void main() { | ||||
|   // final instance = RemoveAssetsDto(); | ||||
| 
 | ||||
|   group('test RemoveAssetsDto', () { | ||||
|     // List<String> assetIds (default value: const []) | ||||
|     test('to test the property `assetIds`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -278,7 +278,7 @@ | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/RemoveAssetsDto" | ||||
|                 "$ref": "#/components/schemas/BulkIdsDto" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
| @@ -289,7 +289,10 @@ | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/AlbumResponseDto" | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/BulkIdResponseDto" | ||||
|                   }, | ||||
|                   "type": "array" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
| @@ -336,7 +339,7 @@ | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/AddAssetsDto" | ||||
|                 "$ref": "#/components/schemas/BulkIdsDto" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
| @@ -347,7 +350,10 @@ | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "$ref": "#/components/schemas/AddAssetsResponseDto" | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/BulkIdResponseDto" | ||||
|                   }, | ||||
|                   "type": "array" | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
| @@ -4535,42 +4541,6 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AddAssetsDto": { | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
|             "items": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetIds" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AddAssetsResponseDto": { | ||||
|         "properties": { | ||||
|           "album": { | ||||
|             "$ref": "#/components/schemas/AlbumResponseDto" | ||||
|           }, | ||||
|           "alreadyInAlbum": { | ||||
|             "items": { | ||||
|               "type": "string" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "successfullyAdded": { | ||||
|             "type": "integer" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "successfullyAdded", | ||||
|           "alreadyInAlbum" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AddUsersDto": { | ||||
|         "properties": { | ||||
|           "sharedUserIds": { | ||||
| @@ -5093,6 +5063,21 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "BulkIdsDto": { | ||||
|         "properties": { | ||||
|           "ids": { | ||||
|             "items": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ids" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "ChangePasswordDto": { | ||||
|         "properties": { | ||||
|           "newPassword": { | ||||
| @@ -6055,21 +6040,6 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "RemoveAssetsDto": { | ||||
|         "properties": { | ||||
|           "assetIds": { | ||||
|             "items": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "assetIds" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "SearchAlbumResponseDto": { | ||||
|         "properties": { | ||||
|           "count": { | ||||
|   | ||||
| @@ -12,9 +12,10 @@ export enum Permission { | ||||
|   ASSET_DOWNLOAD = 'asset.download', | ||||
|  | ||||
|   // ALBUM_CREATE = 'album.create', | ||||
|   // ALBUM_READ = 'album.read', | ||||
|   ALBUM_READ = 'album.read', | ||||
|   ALBUM_UPDATE = 'album.update', | ||||
|   ALBUM_DELETE = 'album.delete', | ||||
|   ALBUM_REMOVE_ASSET = 'album.removeAsset', | ||||
|   ALBUM_SHARE = 'album.share', | ||||
|   ALBUM_DOWNLOAD = 'album.download', | ||||
|  | ||||
| @@ -39,6 +40,16 @@ export class AccessCore { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) { | ||||
|     for (const { permission, id } of permissions) { | ||||
|       const hasAccess = await this.hasPermission(authUser, permission, id); | ||||
|       if (hasAccess) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { | ||||
|     ids = Array.isArray(ids) ? ids : [ids]; | ||||
|  | ||||
| @@ -76,12 +87,11 @@ export class AccessCore { | ||||
|         // TODO: fix this to not use authUser.id for shared link access control | ||||
|         return this.repository.asset.hasOwnerAccess(authUser.id, id); | ||||
|  | ||||
|       case Permission.ALBUM_DOWNLOAD: { | ||||
|         return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id)); | ||||
|       } | ||||
|       case Permission.ALBUM_READ: | ||||
|         return this.repository.album.hasSharedLinkAccess(sharedLinkId, id); | ||||
|  | ||||
|       // case Permission.ALBUM_READ: | ||||
|       //   return this.repository.album.hasSharedLinkAccess(sharedLinkId, id); | ||||
|       case Permission.ALBUM_DOWNLOAD: | ||||
|         return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id)); | ||||
|  | ||||
|       default: | ||||
|         return false; | ||||
| @@ -122,8 +132,11 @@ export class AccessCore { | ||||
|           (await this.repository.asset.hasPartnerAccess(authUser.id, id)) | ||||
|         ); | ||||
|  | ||||
|       // case Permission.ALBUM_READ: | ||||
|       //   return this.repository.album.hasOwnerAccess(authUser.id, id); | ||||
|       case Permission.ALBUM_READ: | ||||
|         return ( | ||||
|           (await this.repository.album.hasOwnerAccess(authUser.id, id)) || | ||||
|           (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) | ||||
|         ); | ||||
|  | ||||
|       case Permission.ALBUM_UPDATE: | ||||
|         return this.repository.album.hasOwnerAccess(authUser.id, id); | ||||
| @@ -140,13 +153,17 @@ export class AccessCore { | ||||
|           (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) | ||||
|         ); | ||||
|  | ||||
|       case Permission.ALBUM_REMOVE_ASSET: | ||||
|         return this.repository.album.hasOwnerAccess(authUser.id, id); | ||||
|  | ||||
|       case Permission.LIBRARY_READ: | ||||
|         return authUser.id === id || (await this.repository.library.hasPartnerAccess(authUser.id, id)); | ||||
|  | ||||
|       case Permission.LIBRARY_DOWNLOAD: | ||||
|         return authUser.id === id; | ||||
|     } | ||||
|  | ||||
|       default: | ||||
|         return false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export interface AlbumAssetCount { | ||||
| } | ||||
|  | ||||
| export interface IAlbumRepository { | ||||
|   getById(id: string): Promise<AlbumEntity | null>; | ||||
|   getByIds(ids: string[]): Promise<AlbumEntity[]>; | ||||
|   getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>; | ||||
|   hasAsset(id: string, assetId: string): Promise<boolean>; | ||||
| @@ -21,4 +22,5 @@ export interface IAlbumRepository { | ||||
|   create(album: Partial<AlbumEntity>): Promise<AlbumEntity>; | ||||
|   update(album: Partial<AlbumEntity>): Promise<AlbumEntity>; | ||||
|   delete(album: AlbumEntity): Promise<void>; | ||||
|   updateThumbnails(): Promise<number | undefined>; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { BadRequestException } from '@nestjs/common'; | ||||
| import { | ||||
|   albumStub, | ||||
|   assetStub, | ||||
|   authStub, | ||||
|   IAccessRepositoryMock, | ||||
|   newAccessRepositoryMock, | ||||
| @@ -11,7 +12,7 @@ import { | ||||
|   userStub, | ||||
| } from '@test'; | ||||
| import _ from 'lodash'; | ||||
| import { IAssetRepository } from '../asset'; | ||||
| import { BulkIdErrorReason, IAssetRepository } from '../asset'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { IUserRepository } from '../user'; | ||||
| import { IAlbumRepository } from './album.repository'; | ||||
| @@ -202,7 +203,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|   describe('update', () => { | ||||
|     it('should prevent updating an album that does not exist', async () => { | ||||
|       albumMock.getByIds.mockResolvedValue([]); | ||||
|       albumMock.getById.mockResolvedValue(null); | ||||
|  | ||||
|       await expect( | ||||
|         sut.update(authStub.user1, 'invalid-id', { | ||||
| @@ -224,7 +225,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should require a valid thumbnail asset id', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.oneAsset]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|       albumMock.update.mockResolvedValue(albumStub.oneAsset); | ||||
|       albumMock.hasAsset.mockResolvedValue(false); | ||||
|  | ||||
| @@ -241,7 +242,7 @@ describe(AlbumService.name, () => { | ||||
|     it('should allow the owner to update the album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|  | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.oneAsset]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|       albumMock.update.mockResolvedValue(albumStub.oneAsset); | ||||
|  | ||||
|       await sut.update(authStub.admin, albumStub.oneAsset.id, { | ||||
| @@ -263,7 +264,7 @@ describe(AlbumService.name, () => { | ||||
|   describe('delete', () => { | ||||
|     it('should throw an error for an album not found', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([]); | ||||
|       albumMock.getById.mockResolvedValue(null); | ||||
|  | ||||
|       await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
| @@ -274,7 +275,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should not let a shared user delete the album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|  | ||||
|       await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
| @@ -285,7 +286,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should let the owner delete an album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.empty]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.empty); | ||||
|  | ||||
|       await sut.delete(authStub.admin, albumStub.empty.id); | ||||
|  | ||||
| @@ -305,7 +306,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should throw an error if the userId is already added', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       await expect( | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
| @@ -314,7 +315,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should throw an error if the userId does not exist', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       userMock.get.mockResolvedValue(null); | ||||
|       await expect( | ||||
|         sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-3'] }), | ||||
| @@ -324,7 +325,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should add valid shared users', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([_.cloneDeep(albumStub.sharedWithAdmin)]); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); | ||||
|       albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); | ||||
|       userMock.get.mockResolvedValue(userStub.user2); | ||||
|       await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.user2.id] }); | ||||
| @@ -339,14 +340,14 @@ describe(AlbumService.name, () => { | ||||
|   describe('removeUser', () => { | ||||
|     it('should require a valid album id', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([]); | ||||
|       albumMock.getById.mockResolvedValue(null); | ||||
|       await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should remove a shared user from an owned album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); | ||||
|  | ||||
|       await expect( | ||||
|         sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id), | ||||
| @@ -362,7 +363,7 @@ describe(AlbumService.name, () => { | ||||
|  | ||||
|     it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithMultiple]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple); | ||||
|  | ||||
|       await expect( | ||||
|         sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.id), | ||||
| @@ -373,7 +374,7 @@ describe(AlbumService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should allow a shared user to remove themselves', async () => { | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); | ||||
|  | ||||
|       await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.id); | ||||
|  | ||||
| @@ -386,7 +387,7 @@ describe(AlbumService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should allow a shared user to remove themselves using "me"', async () => { | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.sharedWithUser]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); | ||||
|  | ||||
|       await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me'); | ||||
|  | ||||
| @@ -399,7 +400,7 @@ describe(AlbumService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should not allow the owner to be removed', async () => { | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.empty]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.empty); | ||||
|  | ||||
|       await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.id)).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
| @@ -409,7 +410,7 @@ describe(AlbumService.name, () => { | ||||
|     }); | ||||
|  | ||||
|     it('should throw an error for a user not in the album', async () => { | ||||
|       albumMock.getByIds.mockResolvedValue([albumStub.empty]); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.empty); | ||||
|  | ||||
|       await expect(sut.removeUser(authStub.admin, albumStub.empty.id, 'user-3')).rejects.toBeInstanceOf( | ||||
|         BadRequestException, | ||||
| @@ -418,4 +419,301 @@ describe(AlbumService.name, () => { | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getAlbumInfo', () => { | ||||
|     it('should get a shared album', async () => { | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|  | ||||
|       await sut.get(authStub.admin, albumStub.oneAsset.id); | ||||
|  | ||||
|       expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id); | ||||
|       expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); | ||||
|     }); | ||||
|  | ||||
|     it('should get a shared album via a shared link', async () => { | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|       accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); | ||||
|  | ||||
|       await sut.get(authStub.adminSharedLink, 'album-123'); | ||||
|  | ||||
|       expect(albumMock.getById).toHaveBeenCalledWith('album-123'); | ||||
|       expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( | ||||
|         authStub.adminSharedLink.sharedLinkId, | ||||
|         'album-123', | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should get a shared album via shared with user', async () => { | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); | ||||
|  | ||||
|       await sut.get(authStub.user1, 'album-123'); | ||||
|  | ||||
|       expect(albumMock.getById).toHaveBeenCalledWith('album-123'); | ||||
|       expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123'); | ||||
|     }); | ||||
|  | ||||
|     it('should throw an error for no access', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); | ||||
|  | ||||
|       await expect(sut.get(authStub.admin, 'album-123')).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); | ||||
|       expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('addAssets', () => { | ||||
|     it('should allow the owner to add assets', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-1' }, | ||||
|         { success: true, id: 'asset-2' }, | ||||
|         { success: true, id: 'asset-3' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], | ||||
|         albumThumbnailAssetId: 'asset-1', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should not set the thumbnail if the album has one already', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })); | ||||
|  | ||||
|       await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-1' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [{ id: 'asset-1' }], | ||||
|         albumThumbnailAssetId: 'asset-id', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should allow a shared user to add assets', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); | ||||
|  | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.user1, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-1' }, | ||||
|         { success: true, id: 'asset-2' }, | ||||
|         { success: true, id: 'asset-3' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], | ||||
|         albumThumbnailAssetId: 'asset-1', | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should allow a shared link user to add assets', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); | ||||
|       accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-1' }, | ||||
|         { success: true, id: 'asset-2' }, | ||||
|         { success: true, id: 'asset-3' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], | ||||
|         albumThumbnailAssetId: 'asset-1', | ||||
|       }); | ||||
|  | ||||
|       expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( | ||||
|         authStub.adminSharedLink.sharedLinkId, | ||||
|         'album-123', | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it('should allow adding assets shared via partner sharing', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.asset.hasPartnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-1' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [assetStub.image, { id: 'asset-1' }], | ||||
|         albumThumbnailAssetId: 'asset-1', | ||||
|       }); | ||||
|  | ||||
|       expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); | ||||
|     }); | ||||
|  | ||||
|     it('should skip duplicate assets', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ | ||||
|         { success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should skip assets not shared with user', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.asset.hasPartnerAccess.mockResolvedValue(false); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|  | ||||
|       await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ | ||||
|         { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, | ||||
|       ]); | ||||
|  | ||||
|       expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); | ||||
|       expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); | ||||
|     }); | ||||
|  | ||||
|     it('should not allow unauthorized access to the album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(false); | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|  | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(accessMock.album.hasOwnerAccess).toHaveBeenCalled(); | ||||
|       expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should not allow unauthorized shared link access to the album', async () => { | ||||
|       accessMock.album.hasSharedLinkAccess.mockResolvedValue(false); | ||||
|       albumMock.getById.mockResolvedValue(albumStub.oneAsset); | ||||
|  | ||||
|       await expect( | ||||
|         sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|  | ||||
|       expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalled(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('removeAssets', () => { | ||||
|     it('should allow the owner to remove assets', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-id' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [], | ||||
|         albumThumbnailAssetId: null, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should skip assets not in the album', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty)); | ||||
|  | ||||
|       await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ | ||||
|         { success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should skip assets without user permission to remove', async () => { | ||||
|       accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); | ||||
|  | ||||
|       await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ | ||||
|         { success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should reset the thumbnail if it is removed', async () => { | ||||
|       accessMock.album.hasOwnerAccess.mockResolvedValue(true); | ||||
|       albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); | ||||
|  | ||||
|       await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ | ||||
|         { success: true, id: 'asset-id' }, | ||||
|       ]); | ||||
|  | ||||
|       expect(albumMock.update).toHaveBeenCalledWith({ | ||||
|         id: 'album-123', | ||||
|         updatedAt: expect.any(Date), | ||||
|         assets: [assetStub.withLocation], | ||||
|         albumThumbnailAssetId: assetStub.withLocation.id, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   // // it('removes assets from shared album (shared with auth user)', async () => { | ||||
|   // //   const albumEntity = _getOwnedSharedAlbum(); | ||||
|   // //   albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|   // //   albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|  | ||||
|   // //   await expect( | ||||
|   // //     sut.removeAssetsFromAlbum( | ||||
|   // //       authUser, | ||||
|   // //       { | ||||
|   // //         ids: ['1'], | ||||
|   // //       }, | ||||
|   // //       albumEntity.id, | ||||
|   // //     ), | ||||
|   // //   ).resolves.toBeUndefined(); | ||||
|   // //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); | ||||
|   // //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { | ||||
|   // //     ids: ['1'], | ||||
|   // //   }); | ||||
|   // // }); | ||||
|  | ||||
|   // it('prevents removing assets from a not owned / shared album', async () => { | ||||
|   //   const albumEntity = _getNotOwnedNotSharedAlbum(); | ||||
|  | ||||
|   //   const albumResponse: AddAssetsResponseDto = { | ||||
|   //     alreadyInAlbum: [], | ||||
|   //     successfullyAdded: 1, | ||||
|   //   }; | ||||
|  | ||||
|   //   const albumId = albumEntity.id; | ||||
|  | ||||
|   //   albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|   //   albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|   //   await expect(sut.removeAssets(authUser, albumId, { ids: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); | ||||
|   // }); | ||||
| }); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities'; | ||||
| import { BadRequestException, Inject, Injectable } from '@nestjs/common'; | ||||
| import { IAssetRepository, mapAsset } from '../asset'; | ||||
| import { AccessCore, IAccessRepository, Permission } from '../access'; | ||||
| import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto, IAssetRepository, mapAsset } from '../asset'; | ||||
| import { AuthUserDto } from '../auth'; | ||||
| import { AccessCore, IAccessRepository, Permission } from '../index'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { IUserRepository } from '../user'; | ||||
| import { AlbumCountResponseDto, AlbumResponseDto, mapAlbum } from './album-response.dto'; | ||||
| @@ -37,7 +37,11 @@ export class AlbumService { | ||||
|   } | ||||
|  | ||||
|   async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> { | ||||
|     await this.updateInvalidThumbnails(); | ||||
|     const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail(); | ||||
|     for (const albumId of invalidAlbumIds) { | ||||
|       const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId); | ||||
|       await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail }); | ||||
|     } | ||||
|  | ||||
|     let albums: AlbumEntity[]; | ||||
|     if (assetId) { | ||||
| @@ -73,15 +77,10 @@ export class AlbumService { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private async updateInvalidThumbnails(): Promise<number> { | ||||
|     const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail(); | ||||
|  | ||||
|     for (const albumId of invalidAlbumIds) { | ||||
|       const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId); | ||||
|       await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail }); | ||||
|     } | ||||
|  | ||||
|     return invalidAlbumIds.length; | ||||
|   async get(authUser: AuthUserDto, id: string) { | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); | ||||
|     await this.albumRepository.updateThumbnails(); | ||||
|     return mapAlbum(await this.findOrFail(id)); | ||||
|   } | ||||
|  | ||||
|   async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> { | ||||
| @@ -107,7 +106,7 @@ export class AlbumService { | ||||
|   async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_UPDATE, id); | ||||
|  | ||||
|     const album = await this.get(id); | ||||
|     const album = await this.findOrFail(id); | ||||
|  | ||||
|     if (dto.albumThumbnailAssetId) { | ||||
|       const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId); | ||||
| @@ -130,7 +129,7 @@ export class AlbumService { | ||||
|   async delete(authUser: AuthUserDto, id: string): Promise<void> { | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_DELETE, id); | ||||
|  | ||||
|     const [album] = await this.albumRepository.getByIds([id]); | ||||
|     const album = await this.albumRepository.getById(id); | ||||
|     if (!album) { | ||||
|       throw new BadRequestException('Album not found'); | ||||
|     } | ||||
| @@ -139,10 +138,88 @@ export class AlbumService { | ||||
|     await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ALBUM, data: { ids: [id] } }); | ||||
|   } | ||||
|  | ||||
|   async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { | ||||
|     const album = await this.findOrFail(id); | ||||
|  | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); | ||||
|  | ||||
|     const results: BulkIdResponseDto[] = []; | ||||
|     for (const id of dto.ids) { | ||||
|       const hasAsset = album.assets.find((asset) => asset.id === id); | ||||
|       if (hasAsset) { | ||||
|         results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE }); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id); | ||||
|       if (!hasAccess) { | ||||
|         results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION }); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       results.push({ id, success: true }); | ||||
|       album.assets.push({ id } as AssetEntity); | ||||
|     } | ||||
|  | ||||
|     const newAsset = results.find(({ success }) => success); | ||||
|     if (newAsset) { | ||||
|       await this.albumRepository.update({ | ||||
|         id, | ||||
|         assets: album.assets, | ||||
|         updatedAt: new Date(), | ||||
|         albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
|   } | ||||
|  | ||||
|   async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { | ||||
|     const album = await this.findOrFail(id); | ||||
|  | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); | ||||
|  | ||||
|     const results: BulkIdResponseDto[] = []; | ||||
|     for (const id of dto.ids) { | ||||
|       const hasAsset = album.assets.find((asset) => asset.id === id); | ||||
|       if (!hasAsset) { | ||||
|         results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND }); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       const hasAccess = await this.access.hasAny(authUser, [ | ||||
|         { permission: Permission.ALBUM_REMOVE_ASSET, id }, | ||||
|         { permission: Permission.ASSET_SHARE, id }, | ||||
|       ]); | ||||
|       if (!hasAccess) { | ||||
|         results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION }); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       results.push({ id, success: true }); | ||||
|       album.assets = album.assets.filter((asset) => asset.id !== id); | ||||
|       if (album.albumThumbnailAssetId === id) { | ||||
|         album.albumThumbnailAssetId = null; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const hasSuccess = results.find(({ success }) => success); | ||||
|     if (hasSuccess) { | ||||
|       await this.albumRepository.update({ | ||||
|         id, | ||||
|         assets: album.assets, | ||||
|         updatedAt: new Date(), | ||||
|         albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
|   } | ||||
|  | ||||
|   async addUsers(authUser: AuthUserDto, id: string, dto: AddUsersDto) { | ||||
|     await this.access.requirePermission(authUser, Permission.ALBUM_SHARE, id); | ||||
|  | ||||
|     const album = await this.get(id); | ||||
|     const album = await this.findOrFail(id); | ||||
|  | ||||
|     for (const userId of dto.sharedUserIds) { | ||||
|       const exists = album.sharedUsers.find((user) => user.id === userId); | ||||
| @@ -172,7 +249,7 @@ export class AlbumService { | ||||
|       userId = authUser.id; | ||||
|     } | ||||
|  | ||||
|     const album = await this.get(id); | ||||
|     const album = await this.findOrFail(id); | ||||
|  | ||||
|     if (album.ownerId === userId) { | ||||
|       throw new BadRequestException('Cannot remove album owner'); | ||||
| @@ -195,8 +272,8 @@ export class AlbumService { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async get(id: string) { | ||||
|     const [album] = await this.albumRepository.getByIds([id]); | ||||
|   private async findOrFail(id: string) { | ||||
|     const album = await this.albumRepository.getById(id); | ||||
|     if (!album) { | ||||
|       throw new BadRequestException('Album not found'); | ||||
|     } | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import { ValidateUUID } from '../../domain.util'; | ||||
|  | ||||
| /** @deprecated Use `BulkIdResponseDto` instead */ | ||||
| export enum AssetIdErrorReason { | ||||
|   DUPLICATE = 'duplicate', | ||||
| @@ -19,6 +21,11 @@ export enum BulkIdErrorReason { | ||||
|   UNKNOWN = 'unknown', | ||||
| } | ||||
|  | ||||
| export class BulkIdsDto { | ||||
|   @ValidateUUID({ each: true }) | ||||
|   ids!: string[]; | ||||
| } | ||||
|  | ||||
| export class BulkIdResponseDto { | ||||
|   id!: string; | ||||
|   success!: boolean; | ||||
|   | ||||
| @@ -1,132 +0,0 @@ | ||||
| import { dataSource } from '@app/infra/database.config'; | ||||
| import { AlbumEntity, AssetEntity } from '@app/infra/entities'; | ||||
| import { Injectable } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { Repository } from 'typeorm'; | ||||
| import { AddAssetsDto } from './dto/add-assets.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; | ||||
|  | ||||
| export interface IAlbumRepository { | ||||
|   get(albumId: string): Promise<AlbumEntity | null>; | ||||
|   removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<number>; | ||||
|   addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>; | ||||
|   updateThumbnails(): Promise<number | undefined>; | ||||
| } | ||||
|  | ||||
| export const IAlbumRepository = 'IAlbumRepository'; | ||||
|  | ||||
| @Injectable() | ||||
| export class AlbumRepository implements IAlbumRepository { | ||||
|   constructor( | ||||
|     @InjectRepository(AlbumEntity) private albumRepository: Repository<AlbumEntity>, | ||||
|     @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, | ||||
|   ) {} | ||||
|  | ||||
|   async get(albumId: string): Promise<AlbumEntity | null> { | ||||
|     return this.albumRepository.findOne({ | ||||
|       where: { id: albumId }, | ||||
|       relations: { | ||||
|         owner: true, | ||||
|         sharedUsers: true, | ||||
|         assets: { | ||||
|           exifInfo: true, | ||||
|         }, | ||||
|         sharedLinks: true, | ||||
|       }, | ||||
|       order: { | ||||
|         assets: { | ||||
|           fileCreatedAt: 'DESC', | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async removeAssets(album: AlbumEntity, removeAssetsDto: RemoveAssetsDto): Promise<number> { | ||||
|     const assetCount = album.assets.length; | ||||
|  | ||||
|     album.assets = album.assets.filter((asset) => { | ||||
|       return !removeAssetsDto.assetIds.includes(asset.id); | ||||
|     }); | ||||
|  | ||||
|     const numRemovedAssets = assetCount - album.assets.length; | ||||
|     if (numRemovedAssets > 0) { | ||||
|       album.updatedAt = new Date(); | ||||
|     } | ||||
|     await this.albumRepository.save(album, {}); | ||||
|  | ||||
|     return numRemovedAssets; | ||||
|   } | ||||
|  | ||||
|   async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> { | ||||
|     const alreadyExisting: string[] = []; | ||||
|  | ||||
|     for (const assetId of addAssetsDto.assetIds) { | ||||
|       // Album already contains that asset | ||||
|       if (album.assets?.some((a) => a.id === assetId)) { | ||||
|         alreadyExisting.push(assetId); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       album.assets.push({ id: assetId } as AssetEntity); | ||||
|     } | ||||
|  | ||||
|     // Add album thumbnail if not exist. | ||||
|     if (!album.albumThumbnailAssetId && album.assets.length > 0) { | ||||
|       album.albumThumbnailAssetId = album.assets[0].id; | ||||
|     } | ||||
|  | ||||
|     const successfullyAdded = addAssetsDto.assetIds.length - alreadyExisting.length; | ||||
|     if (successfullyAdded > 0) { | ||||
|       album.updatedAt = new Date(); | ||||
|     } | ||||
|     await this.albumRepository.save(album); | ||||
|  | ||||
|     return { | ||||
|       successfullyAdded, | ||||
|       alreadyInAlbum: alreadyExisting, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Makes sure all thumbnails for albums are updated by: | ||||
|    * - Removing thumbnails from albums without assets | ||||
|    * - Removing references of thumbnails to assets outside the album | ||||
|    * - Setting a thumbnail when none is set and the album contains assets | ||||
|    * | ||||
|    * @returns Amount of updated album thumbnails or undefined when unknown | ||||
|    */ | ||||
|   async updateThumbnails(): Promise<number | undefined> { | ||||
|     // Subquery for getting a new thumbnail. | ||||
|     const newThumbnail = this.assetRepository | ||||
|       .createQueryBuilder('assets') | ||||
|       .select('albums_assets2.assetsId') | ||||
|       .addFrom('albums_assets_assets', 'albums_assets2') | ||||
|       .where('albums_assets2.assetsId = assets.id') | ||||
|       .andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query | ||||
|       .orderBy('assets.fileCreatedAt', 'DESC') | ||||
|       .limit(1); | ||||
|  | ||||
|     // Using dataSource, because there is no direct access to albums_assets_assets. | ||||
|     const albumHasAssets = dataSource | ||||
|       .createQueryBuilder() | ||||
|       .select('1') | ||||
|       .from('albums_assets_assets', 'albums_assets') | ||||
|       .where('"albums"."id" = "albums_assets"."albumsId"'); | ||||
|  | ||||
|     const albumContainsThumbnail = albumHasAssets | ||||
|       .clone() | ||||
|       .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"'); | ||||
|  | ||||
|     const updateAlbums = this.albumRepository | ||||
|       .createQueryBuilder('albums') | ||||
|       .update(AlbumEntity) | ||||
|       .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` }) | ||||
|       .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`) | ||||
|       .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`); | ||||
|  | ||||
|     const result = await updateAlbums.execute(); | ||||
|  | ||||
|     return result.affected; | ||||
|   } | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| import { AlbumResponseDto, AuthUserDto } from '@app/domain'; | ||||
| import { Body, Controller, Delete, Get, Param, Put } from '@nestjs/common'; | ||||
| import { ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated, AuthUser, SharedLinkRoute } from '../../app.guard'; | ||||
| import { UseValidation } from '../../app.utils'; | ||||
| import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; | ||||
| import { AlbumService } from './album.service'; | ||||
| import { AddAssetsDto } from './dto/add-assets.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; | ||||
|  | ||||
| @ApiTags('Album') | ||||
| @Controller('album') | ||||
| @Authenticated() | ||||
| @UseValidation() | ||||
| export class AlbumController { | ||||
|   constructor(private service: AlbumService) {} | ||||
|  | ||||
|   @SharedLinkRoute() | ||||
|   @Put(':id/assets') | ||||
|   addAssetsToAlbum( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|     @Body() dto: AddAssetsDto, | ||||
|   ): Promise<AddAssetsResponseDto> { | ||||
|     // TODO: Handle nonexistent assetIds. | ||||
|     // TODO: Disallow adding assets of another user to an album. | ||||
|     return this.service.addAssets(authUser, id, dto); | ||||
|   } | ||||
|  | ||||
|   @SharedLinkRoute() | ||||
|   @Get(':id') | ||||
|   getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { | ||||
|     return this.service.get(authUser, id); | ||||
|   } | ||||
|  | ||||
|   @Delete(':id/assets') | ||||
|   removeAssetFromAlbum( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Body() dto: RemoveAssetsDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|   ): Promise<AlbumResponseDto> { | ||||
|     return this.service.removeAssets(authUser, id, dto); | ||||
|   } | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| import { AlbumEntity, AssetEntity } from '@app/infra/entities'; | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AlbumRepository, IAlbumRepository } from './album-repository'; | ||||
| import { AlbumController } from './album.controller'; | ||||
| import { AlbumService } from './album.service'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [TypeOrmModule.forFeature([AlbumEntity, AssetEntity])], | ||||
|   controllers: [AlbumController], | ||||
|   providers: [AlbumService, { provide: IAlbumRepository, useClass: AlbumRepository }], | ||||
| }) | ||||
| export class AlbumModule {} | ||||
| @@ -1,258 +0,0 @@ | ||||
| import { AlbumResponseDto, AuthUserDto, mapUser } from '@app/domain'; | ||||
| import { AlbumEntity, UserEntity } from '@app/infra/entities'; | ||||
| import { ForbiddenException, NotFoundException } from '@nestjs/common'; | ||||
| import { userStub } from '@test'; | ||||
| import { IAlbumRepository } from './album-repository'; | ||||
| import { AlbumService } from './album.service'; | ||||
| import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; | ||||
|  | ||||
| describe('Album service', () => { | ||||
|   let sut: AlbumService; | ||||
|   let albumRepositoryMock: jest.Mocked<IAlbumRepository>; | ||||
|  | ||||
|   const authUser: AuthUserDto = Object.freeze({ | ||||
|     id: '1111', | ||||
|     email: 'auth@test.com', | ||||
|     isAdmin: false, | ||||
|   }); | ||||
|  | ||||
|   const albumOwner: UserEntity = Object.freeze({ | ||||
|     ...authUser, | ||||
|     firstName: 'auth', | ||||
|     lastName: 'user', | ||||
|     createdAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     deletedAt: null, | ||||
|     updatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|     profileImagePath: '', | ||||
|     shouldChangePassword: false, | ||||
|     oauthId: '', | ||||
|     tags: [], | ||||
|     assets: [], | ||||
|     storageLabel: null, | ||||
|     externalPath: null, | ||||
|   }); | ||||
|   const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58'; | ||||
|   const sharedAlbumOwnerId = '2222'; | ||||
|   const sharedAlbumSharedAlsoWithId = '3333'; | ||||
|  | ||||
|   const _getOwnedAlbum = () => { | ||||
|     const albumEntity = new AlbumEntity(); | ||||
|     albumEntity.ownerId = albumOwner.id; | ||||
|     albumEntity.owner = albumOwner; | ||||
|     albumEntity.id = albumId; | ||||
|     albumEntity.albumName = 'name'; | ||||
|     albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z'); | ||||
|     albumEntity.updatedAt = new Date('2022-06-19T23:41:36.910Z'); | ||||
|     albumEntity.sharedUsers = []; | ||||
|     albumEntity.assets = []; | ||||
|     albumEntity.albumThumbnailAssetId = null; | ||||
|     albumEntity.sharedLinks = []; | ||||
|     return albumEntity; | ||||
|   }; | ||||
|  | ||||
|   const _getSharedWithAuthUserAlbum = () => { | ||||
|     const albumEntity = new AlbumEntity(); | ||||
|     albumEntity.ownerId = sharedAlbumOwnerId; | ||||
|     albumEntity.owner = albumOwner; | ||||
|     albumEntity.id = albumId; | ||||
|     albumEntity.albumName = 'name'; | ||||
|     albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z'); | ||||
|     albumEntity.assets = []; | ||||
|     albumEntity.albumThumbnailAssetId = null; | ||||
|     albumEntity.sharedUsers = [ | ||||
|       { | ||||
|         ...userStub.user1, | ||||
|         id: authUser.id, | ||||
|       }, | ||||
|       { | ||||
|         ...userStub.user1, | ||||
|         id: sharedAlbumSharedAlsoWithId, | ||||
|       }, | ||||
|     ]; | ||||
|     albumEntity.sharedLinks = []; | ||||
|  | ||||
|     return albumEntity; | ||||
|   }; | ||||
|  | ||||
|   const _getNotOwnedNotSharedAlbum = () => { | ||||
|     const albumEntity = new AlbumEntity(); | ||||
|     albumEntity.ownerId = '5555'; | ||||
|     albumEntity.id = albumId; | ||||
|     albumEntity.albumName = 'name'; | ||||
|     albumEntity.createdAt = new Date('2022-06-19T23:41:36.910Z'); | ||||
|     albumEntity.sharedUsers = []; | ||||
|     albumEntity.assets = []; | ||||
|     albumEntity.albumThumbnailAssetId = null; | ||||
|  | ||||
|     return albumEntity; | ||||
|   }; | ||||
|  | ||||
|   beforeAll(() => { | ||||
|     albumRepositoryMock = { | ||||
|       addAssets: jest.fn(), | ||||
|       get: jest.fn(), | ||||
|       removeAssets: jest.fn(), | ||||
|       updateThumbnails: jest.fn(), | ||||
|     }; | ||||
|  | ||||
|     sut = new AlbumService(albumRepositoryMock); | ||||
|   }); | ||||
|  | ||||
|   it('gets an owned album', async () => { | ||||
|     const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58'; | ||||
|  | ||||
|     const albumEntity = _getOwnedAlbum(); | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|  | ||||
|     const expectedResult: AlbumResponseDto = { | ||||
|       ownerId: albumOwner.id, | ||||
|       owner: mapUser(albumOwner), | ||||
|       id: albumId, | ||||
|       albumName: 'name', | ||||
|       createdAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|       updatedAt: new Date('2022-06-19T23:41:36.910Z'), | ||||
|       sharedUsers: [], | ||||
|       assets: [], | ||||
|       albumThumbnailAssetId: null, | ||||
|       shared: false, | ||||
|       assetCount: 0, | ||||
|     }; | ||||
|     await expect(sut.get(authUser, albumId)).resolves.toEqual(expectedResult); | ||||
|   }); | ||||
|  | ||||
|   it('gets a shared album', async () => { | ||||
|     const albumEntity = _getSharedWithAuthUserAlbum(); | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|  | ||||
|     const result = await sut.get(authUser, albumId); | ||||
|     expect(result.id).toEqual(albumId); | ||||
|     expect(result.ownerId).toEqual(sharedAlbumOwnerId); | ||||
|     expect(result.shared).toEqual(true); | ||||
|     expect(result.sharedUsers).toHaveLength(2); | ||||
|     expect(result.sharedUsers[0].id).toEqual(authUser.id); | ||||
|     expect(result.sharedUsers[1].id).toEqual(sharedAlbumSharedAlsoWithId); | ||||
|   }); | ||||
|  | ||||
|   it('prevents retrieving an album that is not owned or shared', async () => { | ||||
|     const albumEntity = _getNotOwnedNotSharedAlbum(); | ||||
|     const albumId = albumEntity.id; | ||||
|  | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|     await expect(sut.get(authUser, albumId)).rejects.toBeInstanceOf(ForbiddenException); | ||||
|   }); | ||||
|  | ||||
|   it('throws a not found exception if the album is not found', async () => { | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve(null)); | ||||
|     await expect(sut.get(authUser, '0002')).rejects.toBeInstanceOf(NotFoundException); | ||||
|   }); | ||||
|  | ||||
|   it('adds assets to owned album', async () => { | ||||
|     const albumEntity = _getOwnedAlbum(); | ||||
|  | ||||
|     const albumResponse: AddAssetsResponseDto = { | ||||
|       alreadyInAlbum: [], | ||||
|       successfullyAdded: 1, | ||||
|     }; | ||||
|  | ||||
|     const albumId = albumEntity.id; | ||||
|  | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto; | ||||
|  | ||||
|     // TODO: stub and expect album rendered | ||||
|     expect(result.album?.id).toEqual(albumId); | ||||
|   }); | ||||
|  | ||||
|   it('adds assets to shared album (shared with auth user)', async () => { | ||||
|     const albumEntity = _getSharedWithAuthUserAlbum(); | ||||
|  | ||||
|     const albumResponse: AddAssetsResponseDto = { | ||||
|       alreadyInAlbum: [], | ||||
|       successfullyAdded: 1, | ||||
|     }; | ||||
|  | ||||
|     const albumId = albumEntity.id; | ||||
|  | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     const result = (await sut.addAssets(authUser, albumId, { assetIds: ['1'] })) as AddAssetsResponseDto; | ||||
|  | ||||
|     // TODO: stub and expect album rendered | ||||
|     expect(result.album?.id).toEqual(albumId); | ||||
|   }); | ||||
|  | ||||
|   it('prevents adding assets to a not owned / shared album', async () => { | ||||
|     const albumEntity = _getNotOwnedNotSharedAlbum(); | ||||
|  | ||||
|     const albumResponse: AddAssetsResponseDto = { | ||||
|       alreadyInAlbum: [], | ||||
|       successfullyAdded: 1, | ||||
|     }; | ||||
|  | ||||
|     const albumId = albumEntity.id; | ||||
|  | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     await expect(sut.addAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); | ||||
|   }); | ||||
|  | ||||
|   // it('removes assets from owned album', async () => { | ||||
|   //   const albumEntity = _getOwnedAlbum(); | ||||
|   //   albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|   //   albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|  | ||||
|   //   await expect( | ||||
|   //     sut.removeAssetsFromAlbum( | ||||
|   //       authUser, | ||||
|   //       { | ||||
|   //         assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'], | ||||
|   //       }, | ||||
|   //       albumEntity.id, | ||||
|   //     ), | ||||
|   //   ).resolves.toBeUndefined(); | ||||
|   //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); | ||||
|   //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { | ||||
|   //     assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'], | ||||
|   //   }); | ||||
|   // }); | ||||
|  | ||||
|   // it('removes assets from shared album (shared with auth user)', async () => { | ||||
|   //   const albumEntity = _getOwnedSharedAlbum(); | ||||
|   //   albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|   //   albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|  | ||||
|   //   await expect( | ||||
|   //     sut.removeAssetsFromAlbum( | ||||
|   //       authUser, | ||||
|   //       { | ||||
|   //         assetIds: ['1'], | ||||
|   //       }, | ||||
|   //       albumEntity.id, | ||||
|   //     ), | ||||
|   //   ).resolves.toBeUndefined(); | ||||
|   //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); | ||||
|   //   expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { | ||||
|   //     assetIds: ['1'], | ||||
|   //   }); | ||||
|   // }); | ||||
|  | ||||
|   it('prevents removing assets from a not owned / shared album', async () => { | ||||
|     const albumEntity = _getNotOwnedNotSharedAlbum(); | ||||
|  | ||||
|     const albumResponse: AddAssetsResponseDto = { | ||||
|       alreadyInAlbum: [], | ||||
|       successfullyAdded: 1, | ||||
|     }; | ||||
|  | ||||
|     const albumId = albumEntity.id; | ||||
|  | ||||
|     albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     await expect(sut.removeAssets(authUser, albumId, { assetIds: ['1'] })).rejects.toBeInstanceOf(ForbiddenException); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,72 +0,0 @@ | ||||
| import { AlbumResponseDto, AuthUserDto, mapAlbum } from '@app/domain'; | ||||
| import { AlbumEntity } from '@app/infra/entities'; | ||||
| import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; | ||||
| import { IAlbumRepository } from './album-repository'; | ||||
| import { AddAssetsDto } from './dto/add-assets.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; | ||||
|  | ||||
| @Injectable() | ||||
| export class AlbumService { | ||||
|   private logger = new Logger(AlbumService.name); | ||||
|  | ||||
|   constructor(@Inject(IAlbumRepository) private repository: IAlbumRepository) {} | ||||
|  | ||||
|   private async _getAlbum({ | ||||
|     authUser, | ||||
|     albumId, | ||||
|     validateIsOwner = true, | ||||
|   }: { | ||||
|     authUser: AuthUserDto; | ||||
|     albumId: string; | ||||
|     validateIsOwner?: boolean; | ||||
|   }): Promise<AlbumEntity> { | ||||
|     await this.repository.updateThumbnails(); | ||||
|  | ||||
|     const album = await this.repository.get(albumId); | ||||
|     if (!album) { | ||||
|       throw new NotFoundException('Album Not Found'); | ||||
|     } | ||||
|     const isOwner = album.ownerId == authUser.id; | ||||
|  | ||||
|     if (validateIsOwner && !isOwner) { | ||||
|       throw new ForbiddenException('Unauthorized Album Access'); | ||||
|     } else if (!isOwner && !album.sharedUsers?.some((user) => user.id == authUser.id)) { | ||||
|       throw new ForbiddenException('Unauthorized Album Access'); | ||||
|     } | ||||
|     return album; | ||||
|   } | ||||
|  | ||||
|   async get(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> { | ||||
|     const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); | ||||
|     return mapAlbum(album); | ||||
|   } | ||||
|  | ||||
|   async removeAssets(authUser: AuthUserDto, albumId: string, dto: RemoveAssetsDto): Promise<AlbumResponseDto> { | ||||
|     const album = await this._getAlbum({ authUser, albumId }); | ||||
|     const deletedCount = await this.repository.removeAssets(album, dto); | ||||
|     const newAlbum = await this._getAlbum({ authUser, albumId }); | ||||
|  | ||||
|     if (deletedCount !== dto.assetIds.length) { | ||||
|       throw new BadRequestException('Some assets were not found in the album'); | ||||
|     } | ||||
|  | ||||
|     return mapAlbum(newAlbum); | ||||
|   } | ||||
|  | ||||
|   async addAssets(authUser: AuthUserDto, albumId: string, dto: AddAssetsDto): Promise<AddAssetsResponseDto> { | ||||
|     if (authUser.isPublicUser && !authUser.isAllowUpload) { | ||||
|       this.logger.warn('Deny public user attempt to add asset to album'); | ||||
|       throw new ForbiddenException('Public user is not allowed to upload'); | ||||
|     } | ||||
|  | ||||
|     const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); | ||||
|     const result = await this.repository.addAssets(album, dto); | ||||
|     const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); | ||||
|  | ||||
|     return { | ||||
|       ...result, | ||||
|       album: mapAlbum(newAlbum), | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { ValidateUUID } from '@app/domain'; | ||||
|  | ||||
| export class AddAssetsDto { | ||||
|   @ValidateUUID({ each: true }) | ||||
|   assetIds!: string[]; | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { ValidateUUID } from '@app/domain'; | ||||
|  | ||||
| export class AddUsersDto { | ||||
|   @ValidateUUID({ each: true }) | ||||
|   sharedUserIds!: string[]; | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| import { ValidateUUID } from '@app/domain'; | ||||
|  | ||||
| export class RemoveAssetsDto { | ||||
|   @ValidateUUID({ each: true }) | ||||
|   assetIds!: string[]; | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| import { AlbumResponseDto } from '@app/domain'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
|  | ||||
| export class AddAssetsResponseDto { | ||||
|   @ApiProperty({ type: 'integer' }) | ||||
|   successfullyAdded!: number; | ||||
|  | ||||
|   @ApiProperty() | ||||
|   alreadyInAlbum!: string[]; | ||||
|  | ||||
|   @ApiProperty() | ||||
|   album?: AlbumResponseDto; | ||||
| } | ||||
| @@ -5,7 +5,6 @@ import { Module } from '@nestjs/common'; | ||||
| import { APP_GUARD } from '@nestjs/core'; | ||||
| import { ScheduleModule } from '@nestjs/schedule'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { AlbumModule } from './api-v1/album/album.module'; | ||||
| import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository'; | ||||
| import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller'; | ||||
| import { AssetService } from './api-v1/asset/asset.service'; | ||||
| @@ -34,7 +33,6 @@ import { | ||||
|   imports: [ | ||||
|     // | ||||
|     DomainModule.register({ imports: [InfraModule] }), | ||||
|     AlbumModule, | ||||
|     ScheduleModule.forRoot(), | ||||
|     TypeOrmModule.forFeature([AssetEntity, ExifEntity]), | ||||
|   ], | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import { | ||||
|   AlbumCountResponseDto, | ||||
|   AlbumService, | ||||
|   AuthUserDto, | ||||
|   BulkIdResponseDto, | ||||
|   BulkIdsDto, | ||||
|   CreateAlbumDto, | ||||
|   UpdateAlbumDto, | ||||
| } from '@app/domain'; | ||||
| @@ -10,7 +12,7 @@ import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto'; | ||||
| import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; | ||||
| import { ApiTags } from '@nestjs/swagger'; | ||||
| import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe'; | ||||
| import { Authenticated, AuthUser } from '../app.guard'; | ||||
| import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard'; | ||||
| import { UseValidation } from '../app.utils'; | ||||
| import { UUIDParamDto } from './dto/uuid-param.dto'; | ||||
|  | ||||
| @@ -36,6 +38,12 @@ export class AlbumController { | ||||
|     return this.service.create(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @SharedLinkRoute() | ||||
|   @Get(':id') | ||||
|   getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto) { | ||||
|     return this.service.get(authUser, id); | ||||
|   } | ||||
|  | ||||
|   @Patch(':id') | ||||
|   updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) { | ||||
|     return this.service.update(authUser, id, dto); | ||||
| @@ -46,6 +54,25 @@ export class AlbumController { | ||||
|     return this.service.delete(authUser, id); | ||||
|   } | ||||
|  | ||||
|   @SharedLinkRoute() | ||||
|   @Put(':id/assets') | ||||
|   addAssetsToAlbum( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|     @Body() dto: BulkIdsDto, | ||||
|   ): Promise<BulkIdResponseDto[]> { | ||||
|     return this.service.addAssets(authUser, id, dto); | ||||
|   } | ||||
|  | ||||
|   @Delete(':id/assets') | ||||
|   removeAssetFromAlbum( | ||||
|     @AuthUser() authUser: AuthUserDto, | ||||
|     @Body() dto: BulkIdsDto, | ||||
|     @Param() { id }: UUIDParamDto, | ||||
|   ): Promise<BulkIdResponseDto[]> { | ||||
|     return this.service.removeAssets(authUser, id, dto); | ||||
|   } | ||||
|  | ||||
|   @Put(':id/users') | ||||
|   addUsersToAlbum(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: AddUsersDto) { | ||||
|     return this.service.addUsers(authUser, id, dto); | ||||
|   | ||||
| @@ -3,11 +3,35 @@ import { Injectable } from '@nestjs/common'; | ||||
| import { InjectRepository } from '@nestjs/typeorm'; | ||||
| import { In, IsNull, Not, Repository } from 'typeorm'; | ||||
| import { dataSource } from '../database.config'; | ||||
| import { AlbumEntity } from '../entities'; | ||||
| import { AlbumEntity, AssetEntity } from '../entities'; | ||||
|  | ||||
| @Injectable() | ||||
| export class AlbumRepository implements IAlbumRepository { | ||||
|   constructor(@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>) {} | ||||
|   constructor( | ||||
|     @InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>, | ||||
|     @InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>, | ||||
|   ) {} | ||||
|  | ||||
|   getById(id: string): Promise<AlbumEntity | null> { | ||||
|     return this.repository.findOne({ | ||||
|       where: { | ||||
|         id, | ||||
|       }, | ||||
|       relations: { | ||||
|         owner: true, | ||||
|         sharedUsers: true, | ||||
|         assets: { | ||||
|           exifInfo: true, | ||||
|         }, | ||||
|         sharedLinks: true, | ||||
|       }, | ||||
|       order: { | ||||
|         assets: { | ||||
|           fileCreatedAt: 'DESC', | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   getByIds(ids: string[]): Promise<AlbumEntity[]> { | ||||
|     return this.repository.find({ | ||||
| @@ -161,4 +185,46 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Makes sure all thumbnails for albums are updated by: | ||||
|    * - Removing thumbnails from albums without assets | ||||
|    * - Removing references of thumbnails to assets outside the album | ||||
|    * - Setting a thumbnail when none is set and the album contains assets | ||||
|    * | ||||
|    * @returns Amount of updated album thumbnails or undefined when unknown | ||||
|    */ | ||||
|   async updateThumbnails(): Promise<number | undefined> { | ||||
|     // Subquery for getting a new thumbnail. | ||||
|     const newThumbnail = this.assetRepository | ||||
|       .createQueryBuilder('assets') | ||||
|       .select('albums_assets2.assetsId') | ||||
|       .addFrom('albums_assets_assets', 'albums_assets2') | ||||
|       .where('albums_assets2.assetsId = assets.id') | ||||
|       .andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query | ||||
|       .orderBy('assets.fileCreatedAt', 'DESC') | ||||
|       .limit(1); | ||||
|  | ||||
|     // Using dataSource, because there is no direct access to albums_assets_assets. | ||||
|     const albumHasAssets = dataSource | ||||
|       .createQueryBuilder() | ||||
|       .select('1') | ||||
|       .from('albums_assets_assets', 'albums_assets') | ||||
|       .where('"albums"."id" = "albums_assets"."albumsId"'); | ||||
|  | ||||
|     const albumContainsThumbnail = albumHasAssets | ||||
|       .clone() | ||||
|       .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"'); | ||||
|  | ||||
|     const updateAlbums = this.repository | ||||
|       .createQueryBuilder('albums') | ||||
|       .update(AlbumEntity) | ||||
|       .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` }) | ||||
|       .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`) | ||||
|       .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`); | ||||
|  | ||||
|     const result = await updateAlbums.execute(); | ||||
|  | ||||
|     return result.affected; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										13
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -69,6 +69,19 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|   }), | ||||
|   twoAssets: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-4a', | ||||
|     albumName: 'Album with two assets', | ||||
|     ownerId: authStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     assets: [assetStub.image, assetStub.withLocation], | ||||
|     albumThumbnailAsset: assetStub.image, | ||||
|     albumThumbnailAssetId: assetStub.image.id, | ||||
|     createdAt: new Date(), | ||||
|     updatedAt: new Date(), | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|   }), | ||||
|   emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-5', | ||||
|     albumName: 'Empty album with invalid thumbnail', | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { IAlbumRepository } from '@app/domain'; | ||||
|  | ||||
| export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => { | ||||
|   return { | ||||
|     getById: jest.fn(), | ||||
|     getByIds: jest.fn(), | ||||
|     getByAssetId: jest.fn(), | ||||
|     getAssetCountForIds: jest.fn(), | ||||
| @@ -15,5 +16,6 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => { | ||||
|     create: jest.fn(), | ||||
|     update: jest.fn(), | ||||
|     delete: jest.fn(), | ||||
|     updateThumbnails: jest.fn(), | ||||
|   }; | ||||
| }; | ||||
|   | ||||
							
								
								
									
										120
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										120
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -99,44 +99,6 @@ export interface APIKeyUpdateDto { | ||||
|      */ | ||||
|     'name': string; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AddAssetsDto | ||||
|  */ | ||||
| export interface AddAssetsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AddAssetsDto | ||||
|      */ | ||||
|     'assetIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AddAssetsResponseDto | ||||
|  */ | ||||
| export interface AddAssetsResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AlbumResponseDto} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'album'?: AlbumResponseDto; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'alreadyInAlbum': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof AddAssetsResponseDto | ||||
|      */ | ||||
|     'successfullyAdded': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -821,6 +783,19 @@ export const BulkIdResponseDtoErrorEnum = { | ||||
| 
 | ||||
| export type BulkIdResponseDtoErrorEnum = typeof BulkIdResponseDtoErrorEnum[keyof typeof BulkIdResponseDtoErrorEnum]; | ||||
| 
 | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface BulkIdsDto | ||||
|  */ | ||||
| export interface BulkIdsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof BulkIdsDto | ||||
|      */ | ||||
|     'ids': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -1927,19 +1902,6 @@ export interface QueueStatusDto { | ||||
|      */ | ||||
|     'isPaused': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface RemoveAssetsDto | ||||
|  */ | ||||
| export interface RemoveAssetsDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof RemoveAssetsDto | ||||
|      */ | ||||
|     'assetIds': Array<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -3679,16 +3641,16 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToAlbum: async (id: string, addAssetsDto: AddAssetsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         addAssetsToAlbum: async (id: string, bulkIdsDto: BulkIdsDto, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'id' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'id', id) | ||||
|             // verify required parameter 'addAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'addAssetsDto', addAssetsDto) | ||||
|             // verify required parameter 'bulkIdsDto' is not null or undefined
 | ||||
|             assertParamExists('addAssetsToAlbum', 'bulkIdsDto', bulkIdsDto) | ||||
|             const localVarPath = `/album/{id}/assets` | ||||
|                 .replace(`{${"id"}}`, encodeURIComponent(String(id))); | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
| @@ -3722,7 +3684,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration) | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
| @@ -3999,15 +3961,15 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetFromAlbum: async (id: string, removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         removeAssetFromAlbum: async (id: string, bulkIdsDto: BulkIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'id' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'id', id) | ||||
|             // verify required parameter 'removeAssetsDto' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'removeAssetsDto', removeAssetsDto) | ||||
|             // verify required parameter 'bulkIdsDto' is not null or undefined
 | ||||
|             assertParamExists('removeAssetFromAlbum', 'bulkIdsDto', bulkIdsDto) | ||||
|             const localVarPath = `/album/{id}/assets` | ||||
|                 .replace(`{${"id"}}`, encodeURIComponent(String(id))); | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
| @@ -4037,7 +3999,7 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration) | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(bulkIdsDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
| @@ -4151,13 +4113,13 @@ export const AlbumApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AddAssetsResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, addAssetsDto, key, options); | ||||
|         async addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(id, bulkIdsDto, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -4225,12 +4187,12 @@ export const AlbumApiFp = function(configuration?: Configuration) { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, removeAssetsDto, options); | ||||
|         async removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetFromAlbum(id, bulkIdsDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -4268,13 +4230,13 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {AddAssetsDto} addAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         addAssetsToAlbum(id: string, addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise<AddAssetsResponseDto> { | ||||
|             return localVarFp.addAssetsToAlbum(id, addAssetsDto, key, options).then((request) => request(axios, basePath)); | ||||
|         addAssetsToAlbum(id: string, bulkIdsDto: BulkIdsDto, key?: string, options?: any): AxiosPromise<Array<BulkIdResponseDto>> { | ||||
|             return localVarFp.addAssetsToAlbum(id, bulkIdsDto, key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -4335,12 +4297,12 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath | ||||
|         /** | ||||
|          *  | ||||
|          * @param {string} id  | ||||
|          * @param {RemoveAssetsDto} removeAssetsDto  | ||||
|          * @param {BulkIdsDto} bulkIdsDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         removeAssetFromAlbum(id: string, removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<AlbumResponseDto> { | ||||
|             return localVarFp.removeAssetFromAlbum(id, removeAssetsDto, options).then((request) => request(axios, basePath)); | ||||
|         removeAssetFromAlbum(id: string, bulkIdsDto: BulkIdsDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> { | ||||
|             return localVarFp.removeAssetFromAlbum(id, bulkIdsDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -4380,10 +4342,10 @@ export interface AlbumApiAddAssetsToAlbumRequest { | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AddAssetsDto} | ||||
|      * @type {BulkIdsDto} | ||||
|      * @memberof AlbumApiAddAssetsToAlbum | ||||
|      */ | ||||
|     readonly addAssetsDto: AddAssetsDto | ||||
|     readonly bulkIdsDto: BulkIdsDto | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
| @@ -4499,10 +4461,10 @@ export interface AlbumApiRemoveAssetFromAlbumRequest { | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {RemoveAssetsDto} | ||||
|      * @type {BulkIdsDto} | ||||
|      * @memberof AlbumApiRemoveAssetFromAlbum | ||||
|      */ | ||||
|     readonly removeAssetsDto: RemoveAssetsDto | ||||
|     readonly bulkIdsDto: BulkIdsDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @@ -4562,7 +4524,7 @@ export class AlbumApi extends BaseAPI { | ||||
|      * @memberof AlbumApi | ||||
|      */ | ||||
|     public addAssetsToAlbum(requestParameters: AlbumApiAddAssetsToAlbumRequest, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.addAssetsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AlbumApiFp(this.configuration).addAssetsToAlbum(requestParameters.id, requestParameters.bulkIdsDto, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -4638,7 +4600,7 @@ export class AlbumApi extends BaseAPI { | ||||
|      * @memberof AlbumApi | ||||
|      */ | ||||
|     public removeAssetFromAlbum(requestParameters: AlbumApiRemoveAssetFromAlbumRequest, options?: AxiosRequestConfig) { | ||||
|         return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.removeAssetsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AlbumApiFp(this.configuration).removeAssetFromAlbum(requestParameters.id, requestParameters.bulkIdsDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -92,6 +92,7 @@ | ||||
|  | ||||
|   let multiSelectAsset: Set<AssetResponseDto> = new Set(); | ||||
|   $: isMultiSelectionMode = multiSelectAsset.size > 0; | ||||
|   $: isMultiSelectionUserOwned = Array.from(multiSelectAsset).every((asset) => asset.ownerId === currentUser?.id); | ||||
|  | ||||
|   afterNavigate(({ from }) => { | ||||
|     backUrl = from?.url.pathname ?? '/albums'; | ||||
| @@ -182,24 +183,24 @@ | ||||
|   const createAlbumHandler = async (event: CustomEvent) => { | ||||
|     const { assets }: { assets: AssetResponseDto[] } = event.detail; | ||||
|     try { | ||||
|       const { data } = await api.albumApi.addAssetsToAlbum({ | ||||
|       const { data: results } = await api.albumApi.addAssetsToAlbum({ | ||||
|         id: album.id, | ||||
|         addAssetsDto: { | ||||
|           assetIds: assets.map((a) => a.id), | ||||
|         }, | ||||
|         bulkIdsDto: { ids: assets.map((a) => a.id) }, | ||||
|         key: sharedLink?.key, | ||||
|       }); | ||||
|  | ||||
|       if (data.album) { | ||||
|         album = data.album; | ||||
|       } | ||||
|       const count = results.filter(({ success }) => success).length; | ||||
|       notificationController.show({ | ||||
|         type: NotificationType.Info, | ||||
|         message: `Added ${count} asset${count === 1 ? '' : 's'}`, | ||||
|       }); | ||||
|  | ||||
|       const { data } = await api.albumApi.getAlbumInfo({ id: album.id }); | ||||
|       album = data; | ||||
|  | ||||
|       isShowAssetSelection = false; | ||||
|     } catch (e) { | ||||
|       console.error('Error [createAlbumHandler] ', e); | ||||
|       notificationController.show({ | ||||
|         type: NotificationType.Error, | ||||
|         message: 'Error creating album, check console for more details', | ||||
|       }); | ||||
|       handleError(e, 'Error creating album'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @@ -307,7 +308,7 @@ | ||||
|       {#if sharedLink?.allowDownload || !isPublicShared} | ||||
|         <DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink?.key} /> | ||||
|       {/if} | ||||
|       {#if isOwned} | ||||
|       {#if isOwned || isMultiSelectionUserOwned} | ||||
|         <RemoveFromAlbum bind:album /> | ||||
|       {/if} | ||||
|     </AssetSelectControlBar> | ||||
|   | ||||
| @@ -189,11 +189,8 @@ | ||||
|     isShowAlbumPicker = false; | ||||
|     const album = event.detail.album; | ||||
|  | ||||
|     addAssetsToAlbum(album.id, [asset.id]).then((dto) => { | ||||
|       if (dto.successfullyAdded === 1 && dto.album) { | ||||
|         appearsInAlbums = [...appearsInAlbums, dto.album]; | ||||
|       } | ||||
|     }); | ||||
|     await addAssetsToAlbum(album.id, [asset.id]); | ||||
|     await getAllAlbums(); | ||||
|   }; | ||||
|  | ||||
|   const disableKeyDownEvent = () => { | ||||
|   | ||||
| @@ -44,10 +44,9 @@ | ||||
|   const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => { | ||||
|     showAlbumPicker = false; | ||||
|     const album = event.detail.album; | ||||
|  | ||||
|     const assetIds = Array.from(getAssets()).map((asset) => asset.id); | ||||
|  | ||||
|     addAssetsToAlbum(album.id, assetIds).then(clearSelect); | ||||
|     await addAssetsToAlbum(album.id, assetIds); | ||||
|     clearSelect(); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -17,14 +17,20 @@ | ||||
|  | ||||
|   const removeFromAlbum = async () => { | ||||
|     try { | ||||
|       const { data } = await api.albumApi.removeAssetFromAlbum({ | ||||
|       const { data: results } = await api.albumApi.removeAssetFromAlbum({ | ||||
|         id: album.id, | ||||
|         removeAssetsDto: { | ||||
|           assetIds: Array.from(getAssets()).map((a) => a.id), | ||||
|         }, | ||||
|         bulkIdsDto: { ids: Array.from(getAssets()).map((a) => a.id) }, | ||||
|       }); | ||||
|  | ||||
|       const { data } = await api.albumApi.getAlbumInfo({ id: album.id }); | ||||
|       album = data; | ||||
|  | ||||
|       const count = results.filter(({ success }) => success).length; | ||||
|       notificationController.show({ | ||||
|         type: NotificationType.Info, | ||||
|         message: `Removed ${count} asset${count === 1 ? '' : 's'}`, | ||||
|       }); | ||||
|  | ||||
|       clearSelect(); | ||||
|     } catch (e) { | ||||
|       console.error('Error [album-viewer] [removeAssetFromAlbum]', e); | ||||
|   | ||||
| @@ -1,23 +1,24 @@ | ||||
| import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; | ||||
| import { downloadManager } from '$lib/stores/download'; | ||||
| import { AddAssetsResponseDto, api, AssetApiGetDownloadInfoRequest, AssetResponseDto, DownloadResponseDto } from '@api'; | ||||
| import { api, AssetApiGetDownloadInfoRequest, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto } from '@api'; | ||||
| import { handleError } from './handle-error'; | ||||
|  | ||||
| export const addAssetsToAlbum = async ( | ||||
|   albumId: string, | ||||
|   assetIds: Array<string>, | ||||
|   key: string | undefined = undefined, | ||||
| ): Promise<AddAssetsResponseDto> => | ||||
|   api.albumApi.addAssetsToAlbum({ id: albumId, addAssetsDto: { assetIds }, key }).then(({ data: dto }) => { | ||||
|     if (dto.successfullyAdded > 0) { | ||||
| ): Promise<BulkIdResponseDto[]> => | ||||
|   api.albumApi.addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetIds }, key }).then(({ data: results }) => { | ||||
|     const count = results.filter(({ success }) => success).length; | ||||
|     if (count > 0) { | ||||
|       // This might be 0 if the user tries to add an asset that is already in the album | ||||
|       notificationController.show({ | ||||
|         message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`, | ||||
|         type: NotificationType.Info, | ||||
|         message: `Added ${count} asset${count === 1 ? '' : 's'}`, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return dto; | ||||
|     return results; | ||||
|   }); | ||||
|  | ||||
| const downloadBlob = (data: Blob, filename: string) => { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user