mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 06:31:58 +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 114 additions and 26 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
.idea
|
||||
|
||||
docker/upload
|
|
@ -3,6 +3,7 @@
|
|||
README.md
|
||||
analysis_options.yaml
|
||||
doc/AddAssetsDto.md
|
||||
doc/AddAssetsResponseDto.md
|
||||
doc/AddUsersDto.md
|
||||
doc/AdminSignupResponseDto.md
|
||||
doc/AlbumApi.md
|
||||
|
@ -82,6 +83,7 @@ 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
|
||||
|
@ -137,5 +139,3 @@ lib/model/user_count_response_dto.dart
|
|||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
pubspec.yaml
|
||||
test/check_existing_assets_dto_test.dart
|
||||
test/check_existing_assets_response_dto_test.dart
|
||||
|
|
Binary file not shown.
BIN
mobile/openapi/doc/AddAssetsResponseDto.md
Normal file
BIN
mobile/openapi/doc/AddAssetsResponseDto.md
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/add_assets_response_dto.dart
Normal file
BIN
mobile/openapi/lib/model/add_assets_response_dto.dart
Normal file
Binary file not shown.
BIN
mobile/openapi/test/add_assets_response_dto_test.dart
Normal file
BIN
mobile/openapi/test/add_assets_response_dto_test.dart
Normal file
Binary file not shown.
|
@ -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
|
@ -34,6 +34,31 @@ export interface AddAssetsDto {
|
|||
*/
|
||||
'assetIds': Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AddAssetsResponseDto
|
||||
*/
|
||||
export interface AddAssetsResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AddAssetsResponseDto
|
||||
*/
|
||||
'successfullyAdded': number;
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof AddAssetsResponseDto
|
||||
*/
|
||||
'alreadyInAlbum': Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {AlbumResponseDto}
|
||||
* @memberof AddAssetsResponseDto
|
||||
*/
|
||||
'album'?: AlbumResponseDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -1990,7 +2015,7 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async addAssetsToAlbum(albumId: string, addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlbumResponseDto>> {
|
||||
async addAssetsToAlbum(albumId: string, addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AddAssetsResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToAlbum(albumId, addAssetsDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
|
@ -2105,7 +2130,7 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
addAssetsToAlbum(albumId: string, addAssetsDto: AddAssetsDto, options?: any): AxiosPromise<AlbumResponseDto> {
|
||||
addAssetsToAlbum(albumId: string, addAssetsDto: AddAssetsDto, options?: any): AxiosPromise<AddAssetsResponseDto> {
|
||||
return localVarFp.addAssetsToAlbum(albumId, addAssetsDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -215,8 +215,10 @@
|
|||
const { data } = await api.albumApi.addAssetsToAlbum(album.id, {
|
||||
assetIds: assets.map((a) => a.id)
|
||||
});
|
||||
album = data;
|
||||
|
||||
if (data.album) {
|
||||
album = data.album;
|
||||
}
|
||||
isShowAssetSelection = false;
|
||||
} catch (e) {
|
||||
console.error('Error [createAlbumHandler] ', e);
|
||||
|
@ -233,7 +235,10 @@
|
|||
const { data } = await api.albumApi.addAssetsToAlbum(album.id, {
|
||||
assetIds: assetIds
|
||||
});
|
||||
album = data;
|
||||
|
||||
if (data.album) {
|
||||
album = data.album;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error [assetUploadedToAlbumHandler] ', e);
|
||||
notificationController.show({
|
||||
|
|
Loading…
Reference in a new issue