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:
Matthias Rupp
2022-10-28 21:54:09 +02:00
committed by GitHub
parent 443c842723
commit ea99567805
18 changed files with 314 additions and 30 deletions

View File

@@ -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> {

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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(

View File

@@ -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