mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 07:47:41 +09:00 
			
		
		
		
	chore(server): Improve add to multiple albums via bulk checks and inserts (#21052)
* - add addAssetIdsToAlbums to album repo - update albumService to determine all albums and assets with access and coalesce into one set of album_assets to insert * - remove hasAsset check (unnecessary) * - lint * - cleanup * - remove success counts from addAssetsToAlbums results - Fix tests * open-api * await album update
This commit is contained in:
		| @@ -13,16 +13,10 @@ part of openapi.api; | |||||||
| class AlbumsAddAssetsResponseDto { | class AlbumsAddAssetsResponseDto { | ||||||
|   /// Returns a new [AlbumsAddAssetsResponseDto] instance. |   /// Returns a new [AlbumsAddAssetsResponseDto] instance. | ||||||
|   AlbumsAddAssetsResponseDto({ |   AlbumsAddAssetsResponseDto({ | ||||||
|     required this.albumSuccessCount, |  | ||||||
|     required this.assetSuccessCount, |  | ||||||
|     this.error, |     this.error, | ||||||
|     required this.success, |     required this.success, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   int albumSuccessCount; |  | ||||||
| 
 |  | ||||||
|   int assetSuccessCount; |  | ||||||
| 
 |  | ||||||
|   /// |   /// | ||||||
|   /// Please note: This property should have been non-nullable! Since the specification file |   /// 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 |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
| @@ -35,26 +29,20 @@ class AlbumsAddAssetsResponseDto { | |||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is AlbumsAddAssetsResponseDto && |   bool operator ==(Object other) => identical(this, other) || other is AlbumsAddAssetsResponseDto && | ||||||
|     other.albumSuccessCount == albumSuccessCount && |  | ||||||
|     other.assetSuccessCount == assetSuccessCount && |  | ||||||
|     other.error == error && |     other.error == error && | ||||||
|     other.success == success; |     other.success == success; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
|     // ignore: unnecessary_parenthesis |     // ignore: unnecessary_parenthesis | ||||||
|     (albumSuccessCount.hashCode) + |  | ||||||
|     (assetSuccessCount.hashCode) + |  | ||||||
|     (error == null ? 0 : error!.hashCode) + |     (error == null ? 0 : error!.hashCode) + | ||||||
|     (success.hashCode); |     (success.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'AlbumsAddAssetsResponseDto[albumSuccessCount=$albumSuccessCount, assetSuccessCount=$assetSuccessCount, error=$error, success=$success]'; |   String toString() => 'AlbumsAddAssetsResponseDto[error=$error, success=$success]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
|       json[r'albumSuccessCount'] = this.albumSuccessCount; |  | ||||||
|       json[r'assetSuccessCount'] = this.assetSuccessCount; |  | ||||||
|     if (this.error != null) { |     if (this.error != null) { | ||||||
|       json[r'error'] = this.error; |       json[r'error'] = this.error; | ||||||
|     } else { |     } else { | ||||||
| @@ -73,8 +61,6 @@ class AlbumsAddAssetsResponseDto { | |||||||
|       final json = value.cast<String, dynamic>(); |       final json = value.cast<String, dynamic>(); | ||||||
| 
 | 
 | ||||||
|       return AlbumsAddAssetsResponseDto( |       return AlbumsAddAssetsResponseDto( | ||||||
|         albumSuccessCount: mapValueOfType<int>(json, r'albumSuccessCount')!, |  | ||||||
|         assetSuccessCount: mapValueOfType<int>(json, r'assetSuccessCount')!, |  | ||||||
|         error: BulkIdErrorReason.fromJson(json[r'error']), |         error: BulkIdErrorReason.fromJson(json[r'error']), | ||||||
|         success: mapValueOfType<bool>(json, r'success')!, |         success: mapValueOfType<bool>(json, r'success')!, | ||||||
|       ); |       ); | ||||||
| @@ -124,8 +110,6 @@ class AlbumsAddAssetsResponseDto { | |||||||
| 
 | 
 | ||||||
|   /// The list of required keys that must be present in a JSON. |   /// The list of required keys that must be present in a JSON. | ||||||
|   static const requiredKeys = <String>{ |   static const requiredKeys = <String>{ | ||||||
|     'albumSuccessCount', |  | ||||||
|     'assetSuccessCount', |  | ||||||
|     'success', |     'success', | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10007,12 +10007,6 @@ | |||||||
|       }, |       }, | ||||||
|       "AlbumsAddAssetsResponseDto": { |       "AlbumsAddAssetsResponseDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "albumSuccessCount": { |  | ||||||
|             "type": "integer" |  | ||||||
|           }, |  | ||||||
|           "assetSuccessCount": { |  | ||||||
|             "type": "integer" |  | ||||||
|           }, |  | ||||||
|           "error": { |           "error": { | ||||||
|             "allOf": [ |             "allOf": [ | ||||||
|               { |               { | ||||||
| @@ -10025,8 +10019,6 @@ | |||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|         "required": [ |         "required": [ | ||||||
|           "albumSuccessCount", |  | ||||||
|           "assetSuccessCount", |  | ||||||
|           "success" |           "success" | ||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|   | |||||||
| @@ -389,8 +389,6 @@ export type AlbumsAddAssetsDto = { | |||||||
|     assetIds: string[]; |     assetIds: string[]; | ||||||
| }; | }; | ||||||
| export type AlbumsAddAssetsResponseDto = { | export type AlbumsAddAssetsResponseDto = { | ||||||
|     albumSuccessCount: number; |  | ||||||
|     assetSuccessCount: number; |  | ||||||
|     error?: BulkIdErrorReason; |     error?: BulkIdErrorReason; | ||||||
|     success: boolean; |     success: boolean; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -65,10 +65,6 @@ export class AlbumsAddAssetsDto { | |||||||
|  |  | ||||||
| export class AlbumsAddAssetsResponseDto { | export class AlbumsAddAssetsResponseDto { | ||||||
|   success!: boolean; |   success!: boolean; | ||||||
|   @ApiProperty({ type: 'integer' }) |  | ||||||
|   albumSuccessCount!: number; |  | ||||||
|   @ApiProperty({ type: 'integer' }) |  | ||||||
|   assetSuccessCount!: number; |  | ||||||
|   @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true }) |   @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true }) | ||||||
|   error?: BulkIdErrorReason; |   error?: BulkIdErrorReason; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -321,6 +321,14 @@ export class AlbumRepository { | |||||||
|       .execute(); |       .execute(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @Chunked({ chunkSize: 30_000 }) | ||||||
|  |   async addAssetIdsToAlbums(values: { albumsId: string; assetsId: string }[]): Promise<void> { | ||||||
|  |     if (values.length === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     await this.db.insertInto('album_asset').values(values).execute(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Makes sure all thumbnails for albums are updated by: |    * Makes sure all thumbnails for albums are updated by: | ||||||
|    * - Removing thumbnails from albums without assets |    * - Removing thumbnails from albums without assets | ||||||
|   | |||||||
| @@ -778,9 +778,7 @@ describe(AlbumService.name, () => { | |||||||
|  |  | ||||||
|   describe('addAssetsToAlbums', () => { |   describe('addAssetsToAlbums', () => { | ||||||
|     it('should allow the owner to add assets', async () => { |     it('should allow the owner to add assets', async () => { | ||||||
|       mocks.access.album.checkOwnerAccess |       mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set(['album-321'])); |  | ||||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |       mocks.album.getById | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) |         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) | ||||||
| @@ -792,7 +790,7 @@ describe(AlbumService.name, () => { | |||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(2); |       expect(mocks.album.update).toHaveBeenCalledTimes(2); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { | ||||||
| @@ -805,14 +803,18 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-1', |         albumThumbnailAssetId: 'asset-1', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-123', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-3' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should not set the thumbnail if the album has one already', async () => { |     it('should not set the thumbnail if the album has one already', async () => { | ||||||
|       mocks.access.album.checkOwnerAccess |       mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set(['album-321'])); |  | ||||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |       mocks.album.getById | ||||||
|         .mockResolvedValueOnce(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })) |         .mockResolvedValueOnce(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })) | ||||||
| @@ -824,7 +826,7 @@ describe(AlbumService.name, () => { | |||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(2); |       expect(mocks.album.update).toHaveBeenCalledTimes(2); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { | ||||||
| @@ -837,14 +839,18 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-id', |         albumThumbnailAssetId: 'asset-id', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-123', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-3' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should allow a shared user to add assets', async () => { |     it('should allow a shared user to add assets', async () => { | ||||||
|       mocks.access.album.checkSharedAlbumAccess |       mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set(['album-321'])); |  | ||||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |       mocks.album.getById | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) |         .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) | ||||||
| @@ -856,7 +862,7 @@ describe(AlbumService.name, () => { | |||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(2); |       expect(mocks.album.update).toHaveBeenCalledTimes(2); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { | ||||||
| @@ -869,8 +875,14 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-1', |         albumThumbnailAssetId: 'asset-1', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-123', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-3' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|       expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { |       expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { | ||||||
|         id: 'album-123', |         id: 'album-123', | ||||||
|         recipientId: 'admin_id', |         recipientId: 'admin_id', | ||||||
| @@ -896,18 +908,14 @@ describe(AlbumService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         success: false, |         success: false, | ||||||
|         albumSuccessCount: 0, |         error: BulkIdErrorReason.NO_PERMISSION, | ||||||
|         assetSuccessCount: 0, |  | ||||||
|         error: BulkIdErrorReason.UNKNOWN, |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).not.toHaveBeenCalled(); |       expect(mocks.album.update).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should not allow a shared link user to add assets to multiple albums', async () => { |     it('should not allow a shared link user to add assets to multiple albums', async () => { | ||||||
|       mocks.access.album.checkSharedLinkAccess |       mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set(['album-123'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set()); |  | ||||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |       mocks.album.getById | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) |         .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) | ||||||
| @@ -919,7 +927,7 @@ describe(AlbumService.name, () => { | |||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 1, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(1); |       expect(mocks.album.update).toHaveBeenCalledTimes(1); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { | ||||||
| @@ -927,22 +935,23 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-1', |         albumThumbnailAssetId: 'asset-1', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledTimes(1); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-123', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|       expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { |       expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { | ||||||
|         id: 'album-123', |         id: 'album-123', | ||||||
|         recipientId: 'user-id', |         recipientId: 'user-id', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( |       expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( | ||||||
|         authStub.adminSharedLink.sharedLink?.id, |         authStub.adminSharedLink.sharedLink?.id, | ||||||
|         new Set(['album-123']), |         new Set(['album-123', 'album-321']), | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should allow adding assets shared via partner sharing', async () => { |     it('should allow adding assets shared via partner sharing', async () => { | ||||||
|       mocks.access.album.checkOwnerAccess |       mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set(['album-321'])); |  | ||||||
|       mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |       mocks.album.getById | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) |         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) | ||||||
| @@ -954,7 +963,7 @@ describe(AlbumService.name, () => { | |||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 2, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(2); |       expect(mocks.album.update).toHaveBeenCalledTimes(2); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { | ||||||
| @@ -967,8 +976,14 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-1', |         albumThumbnailAssetId: 'asset-1', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-123', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-123', assetsId: 'asset-3' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|       expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( |       expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( | ||||||
|         authStub.admin.user.id, |         authStub.admin.user.id, | ||||||
|         new Set(['asset-1', 'asset-2', 'asset-3']), |         new Set(['asset-1', 'asset-2', 'asset-3']), | ||||||
| @@ -976,23 +991,21 @@ describe(AlbumService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should skip some duplicate assets', async () => { |     it('should skip some duplicate assets', async () => { | ||||||
|       mocks.access.album.checkOwnerAccess |       mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); | ||||||
|         .mockResolvedValueOnce(new Set(['album-123'])) |  | ||||||
|         .mockResolvedValueOnce(new Set(['album-321'])); |  | ||||||
|       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); |       mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); | ||||||
|       mocks.album.getById |  | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) |  | ||||||
|         .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); |  | ||||||
|       mocks.album.getAssetIds |       mocks.album.getAssetIds | ||||||
|         .mockResolvedValueOnce(new Set(['asset-1', 'asset-2', 'asset-3'])) |         .mockResolvedValueOnce(new Set(['asset-1', 'asset-2', 'asset-3'])) | ||||||
|         .mockResolvedValueOnce(new Set()); |         .mockResolvedValueOnce(new Set()); | ||||||
|  |       mocks.album.getById | ||||||
|  |         .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) | ||||||
|  |         .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); | ||||||
|  |  | ||||||
|       await expect( |       await expect( | ||||||
|         sut.addAssetsToAlbums(authStub.admin, { |         sut.addAssetsToAlbums(authStub.admin, { | ||||||
|           albumIds: ['album-123', 'album-321'], |           albumIds: ['album-123', 'album-321'], | ||||||
|           assetIds: ['asset-1', 'asset-2', 'asset-3'], |           assetIds: ['asset-1', 'asset-2', 'asset-3'], | ||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ success: true, albumSuccessCount: 1, assetSuccessCount: 3 }); |       ).resolves.toEqual({ success: true, error: undefined }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).toHaveBeenCalledTimes(1); |       expect(mocks.album.update).toHaveBeenCalledTimes(1); | ||||||
|       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-321', { |       expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-321', { | ||||||
| @@ -1000,8 +1013,11 @@ describe(AlbumService.name, () => { | |||||||
|         updatedAt: expect.any(Date), |         updatedAt: expect.any(Date), | ||||||
|         albumThumbnailAssetId: 'asset-1', |         albumThumbnailAssetId: 'asset-1', | ||||||
|       }); |       }); | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledTimes(1); |       expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ | ||||||
|       expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-321', ['asset-1', 'asset-2', 'asset-3']); |         { albumsId: 'album-321', assetsId: 'asset-1' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-2' }, | ||||||
|  |         { albumsId: 'album-321', assetsId: 'asset-3' }, | ||||||
|  |       ]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should skip all duplicate assets', async () => { |     it('should skip all duplicate assets', async () => { | ||||||
| @@ -1021,8 +1037,6 @@ describe(AlbumService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         success: false, |         success: false, | ||||||
|         albumSuccessCount: 0, |  | ||||||
|         assetSuccessCount: 0, |  | ||||||
|         error: BulkIdErrorReason.DUPLICATE, |         error: BulkIdErrorReason.DUPLICATE, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
| @@ -1046,9 +1060,7 @@ describe(AlbumService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         success: false, |         success: false, | ||||||
|         albumSuccessCount: 0, |         error: BulkIdErrorReason.NO_PERMISSION, | ||||||
|         assetSuccessCount: 0, |  | ||||||
|         error: BulkIdErrorReason.UNKNOWN, |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).not.toHaveBeenCalled(); |       expect(mocks.album.update).not.toHaveBeenCalled(); | ||||||
| @@ -1076,9 +1088,7 @@ describe(AlbumService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         success: false, |         success: false, | ||||||
|         albumSuccessCount: 0, |         error: BulkIdErrorReason.NO_PERMISSION, | ||||||
|         assetSuccessCount: 0, |  | ||||||
|         error: BulkIdErrorReason.UNKNOWN, |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.album.update).not.toHaveBeenCalled(); |       expect(mocks.album.update).not.toHaveBeenCalled(); | ||||||
| @@ -1099,9 +1109,7 @@ describe(AlbumService.name, () => { | |||||||
|         }), |         }), | ||||||
|       ).resolves.toEqual({ |       ).resolves.toEqual({ | ||||||
|         success: false, |         success: false, | ||||||
|         albumSuccessCount: 0, |         error: BulkIdErrorReason.NO_PERMISSION, | ||||||
|         assetSuccessCount: 0, |  | ||||||
|         error: BulkIdErrorReason.UNKNOWN, |  | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled(); |       expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled(); | ||||||
|   | |||||||
| @@ -191,36 +191,57 @@ export class AlbumService extends BaseService { | |||||||
|   async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> { |   async addAssetsToAlbums(auth: AuthDto, dto: AlbumsAddAssetsDto): Promise<AlbumsAddAssetsResponseDto> { | ||||||
|     const results: AlbumsAddAssetsResponseDto = { |     const results: AlbumsAddAssetsResponseDto = { | ||||||
|       success: false, |       success: false, | ||||||
|       albumSuccessCount: 0, |  | ||||||
|       assetSuccessCount: 0, |  | ||||||
|       error: BulkIdErrorReason.DUPLICATE, |       error: BulkIdErrorReason.DUPLICATE, | ||||||
|     }; |     }; | ||||||
|     const successfulAssetIds: Set<string> = new Set(); |  | ||||||
|     for (const albumId of dto.albumIds) { |  | ||||||
|       try { |  | ||||||
|         const albumResults = await this.addAssets(auth, albumId, { ids: dto.assetIds }); |  | ||||||
|  |  | ||||||
|         let success = false; |     const allowedAlbumIds = await this.checkAccess({ | ||||||
|         for (const res of albumResults) { |       auth, | ||||||
|           if (res.success) { |       permission: Permission.AlbumAssetCreate, | ||||||
|             success = true; |       ids: dto.albumIds, | ||||||
|             results.success = true; |     }); | ||||||
|             results.error = undefined; |     if (allowedAlbumIds.size === 0) { | ||||||
|             successfulAssetIds.add(res.id); |       results.error = BulkIdErrorReason.NO_PERMISSION; | ||||||
|           } else if (results.error && res.error !== BulkIdErrorReason.DUPLICATE) { |       return results; | ||||||
|             results.error = BulkIdErrorReason.UNKNOWN; |     } | ||||||
|           } |  | ||||||
|         } |     const allowedAssetIds = await this.checkAccess({ auth, permission: Permission.AssetShare, ids: dto.assetIds }); | ||||||
|         if (success) { |     if (allowedAssetIds.size === 0) { | ||||||
|           results.albumSuccessCount++; |       results.error = BulkIdErrorReason.NO_PERMISSION; | ||||||
|         } |       return results; | ||||||
|       } catch { |     } | ||||||
|         if (results.error) { |  | ||||||
|           results.error = BulkIdErrorReason.UNKNOWN; |     const albumAssetValues: { albumsId: string; assetsId: string }[] = []; | ||||||
|         } |     const events: { id: string; recipients: string[] }[] = []; | ||||||
|  |     for (const albumId of allowedAlbumIds) { | ||||||
|  |       const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]); | ||||||
|  |       const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id)); | ||||||
|  |       if (notPresentAssetIds.length === 0) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       const album = await this.findOrFail(albumId, { withAssets: false }); | ||||||
|  |       results.error = undefined; | ||||||
|  |       results.success = true; | ||||||
|  |  | ||||||
|  |       for (const assetId of notPresentAssetIds) { | ||||||
|  |         albumAssetValues.push({ albumsId: albumId, assetsId: assetId }); | ||||||
|  |       } | ||||||
|  |       await this.albumRepository.update(albumId, { | ||||||
|  |         id: albumId, | ||||||
|  |         updatedAt: new Date(), | ||||||
|  |         albumThumbnailAssetId: album.albumThumbnailAssetId ?? notPresentAssetIds[0], | ||||||
|  |       }); | ||||||
|  |       const allUsersExceptUs = [...album.albumUsers.map(({ user }) => user.id), album.owner.id].filter( | ||||||
|  |         (userId) => userId !== auth.user.id, | ||||||
|  |       ); | ||||||
|  |       events.push({ id: albumId, recipients: allUsersExceptUs }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await this.albumRepository.addAssetIdsToAlbums(albumAssetValues); | ||||||
|  |     for (const event of events) { | ||||||
|  |       for (const recipientId of event.recipients) { | ||||||
|  |         await this.eventRepository.emit('AlbumUpdate', { id: event.id, recipientId }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     results.assetSuccessCount = successfulAssetIds.size; |  | ||||||
|  |  | ||||||
|     return results; |     return results; | ||||||
|   } |   } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user