mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +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:
parent
443c842723
commit
ea99567805
18 changed files with 314 additions and 30 deletions
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue