mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
refactor(server): update album (#2562)
* refactor: update album * fix: remove unnecessary decorator
This commit is contained in:
@@ -10,6 +10,7 @@ export interface AlbumAssetCount {
|
||||
export interface IAlbumRepository {
|
||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
hasAsset(id: string, assetId: string): Promise<boolean>;
|
||||
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
getInvalidThumbnail(): Promise<string[]>;
|
||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||
@@ -18,5 +19,5 @@ export interface IAlbumRepository {
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
getAll(): Promise<AlbumEntity[]>;
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
save(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import { albumStub, authStub, newAlbumRepositoryMock, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
|
||||
import { IAssetRepository } from '../asset';
|
||||
import { IJobRepository, JobName } from '../job';
|
||||
@@ -89,14 +90,14 @@ describe(AlbumService.name, () => {
|
||||
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
|
||||
]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
|
||||
albumMock.save.mockResolvedValue(albumStub.oneAssetValidThumbnail);
|
||||
albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
|
||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, {});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.save).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('removes the thumbnail for an empty album', async () => {
|
||||
@@ -105,14 +106,14 @@ describe(AlbumService.name, () => {
|
||||
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
|
||||
]);
|
||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
|
||||
albumMock.save.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
|
||||
|
||||
const result = await sut.getAll(authStub.admin, {});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.save).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
@@ -151,4 +152,47 @@ describe(AlbumService.name, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should prevent updating an album that does not exist', async () => {
|
||||
albumMock.getByIds.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.user1, 'invalid-id', {
|
||||
albumName: 'new album name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent updating a not owned album (shared with auth user)', async () => {
|
||||
albumMock.getByIds.mockResolvedValue([albumStub.sharedWithAdmin]);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.admin, albumStub.sharedWithAdmin.id, {
|
||||
albumName: 'new album name',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('should all the owner to update the album', async () => {
|
||||
albumMock.getByIds.mockResolvedValue([albumStub.oneAsset]);
|
||||
albumMock.update.mockResolvedValue(albumStub.oneAsset);
|
||||
|
||||
await sut.update(authStub.admin, albumStub.oneAsset.id, {
|
||||
albumName: 'new album name',
|
||||
});
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-4',
|
||||
albumName: 'new album name',
|
||||
});
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEARCH_INDEX_ALBUM,
|
||||
data: { ids: [albumStub.oneAsset.id] },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
||||
import { IAssetRepository } from '../asset';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { IJobRepository, JobName } from '../job';
|
||||
import { IAlbumRepository } from './album.repository';
|
||||
import { CreateAlbumDto } from './dto/album-create.dto';
|
||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
||||
import { CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
|
||||
import { AlbumResponseDto, mapAlbum } from './response-dto';
|
||||
|
||||
@Injectable()
|
||||
@@ -53,7 +52,7 @@ export class AlbumService {
|
||||
|
||||
for (const albumId of invalidAlbumIds) {
|
||||
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
|
||||
await this.albumRepository.save({ id: albumId, albumThumbnailAsset: newThumbnail });
|
||||
await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail });
|
||||
}
|
||||
|
||||
return invalidAlbumIds.length;
|
||||
@@ -71,4 +70,32 @@ export class AlbumService {
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } });
|
||||
return mapAlbum(album);
|
||||
}
|
||||
|
||||
async update(authUser: AuthUserDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
||||
const [album] = await this.albumRepository.getByIds([id]);
|
||||
if (!album) {
|
||||
throw new BadRequestException('Album not found');
|
||||
}
|
||||
|
||||
if (album.ownerId !== authUser.id) {
|
||||
throw new ForbiddenException('Album not owned by user');
|
||||
}
|
||||
|
||||
if (dto.albumThumbnailAssetId) {
|
||||
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
|
||||
if (!valid) {
|
||||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAlbum = await this.albumRepository.update({
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
||||
|
||||
return mapAlbum(updatedAlbum);
|
||||
}
|
||||
}
|
||||
|
||||
12
server/libs/domain/src/album/dto/album-update.dto.ts
Normal file
12
server/libs/domain/src/album/dto/album-update.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional } from 'class-validator';
|
||||
import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator';
|
||||
|
||||
export class UpdateAlbumDto {
|
||||
@IsOptional()
|
||||
@ApiProperty()
|
||||
albumName?: string;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
albumThumbnailAssetId?: string;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './album-create.dto';
|
||||
export * from './album-update.dto';
|
||||
export * from './get-albums.dto';
|
||||
|
||||
@@ -11,7 +11,8 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
||||
getNotShared: jest.fn(),
|
||||
deleteAll: jest.fn(),
|
||||
getAll: jest.fn(),
|
||||
hasAsset: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -123,11 +123,31 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
});
|
||||
}
|
||||
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
async hasAsset(id: string, assetId: string): Promise<boolean> {
|
||||
const count = await this.repository.count({
|
||||
where: {
|
||||
id,
|
||||
assets: {
|
||||
id: assetId,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Boolean(count);
|
||||
}
|
||||
|
||||
async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
}
|
||||
|
||||
async save(album: Partial<AlbumEntity>) {
|
||||
async update(album: Partial<AlbumEntity>) {
|
||||
return this.save(album);
|
||||
}
|
||||
|
||||
private async save(album: Partial<AlbumEntity>) {
|
||||
const { id } = await this.repository.save(album);
|
||||
return this.repository.findOneOrFail({ where: { id }, relations: { owner: true } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user