mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(server) Extend PUT /album/:id/assets endpoint (#857)
* Add new query parameter to API endpoint that allows adding assets to albums which potentially contain assets that are already part of this album. * Change API endpoint * Generate new APIs * Fixed test Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		| @@ -11,6 +11,7 @@ import { GetAlbumsDto } from './dto/get-albums.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| import { UpdateAlbumDto } from './dto/update-album.dto'; | ||||
| import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; | ||||
| import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto"; | ||||
|  | ||||
| export interface IAlbumRepository { | ||||
|   create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>; | ||||
| @@ -20,7 +21,7 @@ export interface IAlbumRepository { | ||||
|   addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>; | ||||
|   removeUser(album: AlbumEntity, userId: string): Promise<void>; | ||||
|   removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>; | ||||
|   addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>; | ||||
|   addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto>; | ||||
|   updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>; | ||||
|   getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>; | ||||
|   getCountByUserId(userId: string): Promise<AlbumCountResponseDto>; | ||||
| @@ -260,10 +261,16 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity> { | ||||
|   async addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AddAssetsResponseDto> { | ||||
|     const newRecords: AssetAlbumEntity[] = []; | ||||
|     const alreadyExisting: string[] = []; | ||||
|  | ||||
|     for (const assetId of addAssetsDto.assetIds) { | ||||
|       // Album already contains that asset | ||||
|       if (album.assets?.some(a => a.assetId === assetId)) { | ||||
|         alreadyExisting.push(assetId); | ||||
|         continue; | ||||
|       } | ||||
|       const newAssetAlbum = new AssetAlbumEntity(); | ||||
|       newAssetAlbum.assetId = assetId; | ||||
|       newAssetAlbum.albumId = album.id; | ||||
| @@ -278,7 +285,11 @@ export class AlbumRepository implements IAlbumRepository { | ||||
|     } | ||||
|  | ||||
|     await this.assetAlbumRepository.save([...newRecords]); | ||||
|     return this.get(album.id) as Promise<AlbumEntity>; // There is an album for sure | ||||
|  | ||||
|     return { | ||||
|       successfullyAdded: newRecords.length, | ||||
|       alreadyInAlbum: alreadyExisting | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> { | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import { GetAlbumsDto } from './dto/get-albums.dto'; | ||||
| import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; | ||||
| import { AlbumResponseDto } from './response-dto/album-response.dto'; | ||||
| import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; | ||||
| import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto"; | ||||
|  | ||||
| // TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe. | ||||
| @Authenticated() | ||||
| @@ -57,7 +58,7 @@ export class AlbumController { | ||||
|     @GetAuthUser() authUser: AuthUserDto, | ||||
|     @Body(ValidationPipe) addAssetsDto: AddAssetsDto, | ||||
|     @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string, | ||||
|   ) { | ||||
|   ) : Promise<AddAssetsResponseDto> { | ||||
|     return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { AlbumService } from './album.service'; | ||||
| import { IAlbumRepository } from './album-repository'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; | ||||
| import { AlbumEntity } from '@app/database/entities/album.entity'; | ||||
| import { AlbumResponseDto } from './response-dto/album-response.dto'; | ||||
| import { IAssetRepository } from '../asset/asset-repository'; | ||||
| import {AddAssetsResponseDto} from "./response-dto/add-assets-response.dto"; | ||||
| import {IAlbumRepository} from "./album-repository"; | ||||
|  | ||||
| describe('Album service', () => { | ||||
|   let sut: AlbumService; | ||||
| @@ -329,10 +330,16 @@ describe('Album service', () => { | ||||
|  | ||||
|   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<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     const result = await sut.addAssetsToAlbum( | ||||
|       authUser, | ||||
| @@ -340,18 +347,24 @@ describe('Album service', () => { | ||||
|         assetIds: ['1'], | ||||
|       }, | ||||
|       albumId, | ||||
|     ); | ||||
|     ) as AddAssetsResponseDto; | ||||
|  | ||||
|     // TODO: stub and expect album rendered | ||||
|     expect(result.id).toEqual(albumId); | ||||
|     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<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     const result = await sut.addAssetsToAlbum( | ||||
|       authUser, | ||||
| @@ -359,18 +372,24 @@ describe('Album service', () => { | ||||
|         assetIds: ['1'], | ||||
|       }, | ||||
|       albumId, | ||||
|     ); | ||||
|     ) as AddAssetsResponseDto; | ||||
|  | ||||
|     // TODO: stub and expect album rendered | ||||
|     expect(result.id).toEqual(albumId); | ||||
|     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<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     expect( | ||||
|       sut.addAssetsToAlbum( | ||||
| @@ -425,10 +444,16 @@ describe('Album service', () => { | ||||
|  | ||||
|   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<AlbumEntity>(albumEntity)); | ||||
|     albumRepositoryMock.addAssets.mockImplementation(() => Promise.resolve<AddAssetsResponseDto>(albumResponse)); | ||||
|  | ||||
|     expect( | ||||
|       sut.removeAssetsFromAlbum( | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; | ||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||
| import { AddAssetsDto } from './dto/add-assets.dto'; | ||||
| import { CreateAlbumDto } from './dto/create-album.dto'; | ||||
| import { AlbumEntity } from '../../../../../libs/database/src/entities/album.entity'; | ||||
| import { AlbumEntity } from '@app/database/entities/album.entity'; | ||||
| import { AddUsersDto } from './dto/add-users.dto'; | ||||
| import { RemoveAssetsDto } from './dto/remove-assets.dto'; | ||||
| import { UpdateAlbumDto } from './dto/update-album.dto'; | ||||
| @@ -11,6 +10,8 @@ import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response | ||||
| import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository'; | ||||
| import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; | ||||
| import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository'; | ||||
| import { AddAssetsResponseDto } from "./response-dto/add-assets-response.dto"; | ||||
| import {AddAssetsDto} from "./dto/add-assets.dto"; | ||||
|  | ||||
| @Injectable() | ||||
| export class AlbumService { | ||||
| @@ -108,10 +109,15 @@ export class AlbumService { | ||||
|     authUser: AuthUserDto, | ||||
|     addAssetsDto: AddAssetsDto, | ||||
|     albumId: string, | ||||
|   ): Promise<AlbumResponseDto> { | ||||
|   ): Promise<AddAssetsResponseDto> { | ||||
|     const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); | ||||
|     const updatedAlbum = await this._albumRepository.addAssets(album, addAssetsDto); | ||||
|     return mapAlbum(updatedAlbum); | ||||
|     const result = await this._albumRepository.addAssets(album, addAssetsDto); | ||||
|     const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false }); | ||||
|  | ||||
|     return { | ||||
|       ...result, | ||||
|       album: mapAlbum(newAlbum) | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   async updateAlbumInfo( | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| import {ApiProperty} from "@nestjs/swagger"; | ||||
| import {AlbumResponseDto} from "./album-response.dto"; | ||||
|  | ||||
| export class AddAssetsResponseDto { | ||||
|     @ApiProperty({ type: 'integer' }) | ||||
|     successfullyAdded!: number; | ||||
|  | ||||
|     @ApiProperty() | ||||
|     alreadyInAlbum!: string[]; | ||||
|  | ||||
|     @ApiProperty() | ||||
|     album?: AlbumResponseDto; | ||||
| } | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user