refactor: create album (#2555)

This commit is contained in:
Jason Rasmussen 2023-05-24 22:10:45 -04:00 committed by GitHub
parent 83df14d379
commit d827a6182b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 117 additions and 91 deletions

View file

@ -17,5 +17,6 @@ export interface IAlbumRepository {
getNotShared(ownerId: string): Promise<AlbumEntity[]>;
deleteAll(userId: string): Promise<void>;
getAll(): Promise<AlbumEntity[]>;
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
save(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
}

View file

@ -1,5 +1,6 @@
import { albumStub, authStub, newAlbumRepositoryMock, newAssetRepositoryMock } from '../../test';
import { albumStub, authStub, newAlbumRepositoryMock, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
import { IAssetRepository } from '../asset';
import { IJobRepository, JobName } from '../job';
import { IAlbumRepository } from './album.repository';
import { AlbumService } from './album.service';
@ -7,19 +8,21 @@ describe(AlbumService.name, () => {
let sut: AlbumService;
let albumMock: jest.Mocked<IAlbumRepository>;
let assetMock: jest.Mocked<IAssetRepository>;
let jobMock: jest.Mocked<IJobRepository>;
beforeEach(async () => {
albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock();
jobMock = newJobRepositoryMock();
sut = new AlbumService(albumMock, assetMock);
sut = new AlbumService(albumMock, assetMock, jobMock);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('get list of albums', () => {
describe('getAll', () => {
it('gets list of albums for auth user', async () => {
albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
albumMock.getAssetCountForIds.mockResolvedValue([
@ -28,7 +31,7 @@ describe(AlbumService.name, () => {
]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAllAlbums(authStub.admin, {});
const result = await sut.getAll(authStub.admin, {});
expect(result).toHaveLength(2);
expect(result[0].id).toEqual(albumStub.empty.id);
expect(result[1].id).toEqual(albumStub.sharedWithUser.id);
@ -39,7 +42,7 @@ describe(AlbumService.name, () => {
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAllAlbums(authStub.admin, { assetId: albumStub.oneAsset.id });
const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
expect(result).toHaveLength(1);
expect(result[0].id).toEqual(albumStub.oneAsset.id);
expect(albumMock.getByAssetId).toHaveBeenCalledTimes(1);
@ -50,7 +53,7 @@ describe(AlbumService.name, () => {
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAllAlbums(authStub.admin, { shared: true });
const result = await sut.getAll(authStub.admin, { shared: true });
expect(result).toHaveLength(1);
expect(result[0].id).toEqual(albumStub.sharedWithUser.id);
expect(albumMock.getShared).toHaveBeenCalledTimes(1);
@ -61,7 +64,7 @@ describe(AlbumService.name, () => {
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAllAlbums(authStub.admin, { shared: false });
const result = await sut.getAll(authStub.admin, { shared: false });
expect(result).toHaveLength(1);
expect(result[0].id).toEqual(albumStub.empty.id);
expect(albumMock.getNotShared).toHaveBeenCalledTimes(1);
@ -73,7 +76,7 @@ describe(AlbumService.name, () => {
albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
albumMock.getInvalidThumbnail.mockResolvedValue([]);
const result = await sut.getAllAlbums(authStub.admin, {});
const result = await sut.getAll(authStub.admin, {});
expect(result).toHaveLength(1);
expect(result[0].assetCount).toEqual(1);
@ -89,7 +92,7 @@ describe(AlbumService.name, () => {
albumMock.save.mockResolvedValue(albumStub.oneAssetValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
const result = await sut.getAllAlbums(authStub.admin, {});
const result = await sut.getAll(authStub.admin, {});
expect(result).toHaveLength(1);
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
@ -105,10 +108,47 @@ describe(AlbumService.name, () => {
albumMock.save.mockResolvedValue(albumStub.emptyWithValidThumbnail);
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
const result = await sut.getAllAlbums(authStub.admin, {});
const result = await sut.getAll(authStub.admin, {});
expect(result).toHaveLength(1);
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
expect(albumMock.save).toHaveBeenCalledTimes(1);
});
describe('create', () => {
it('creates album', async () => {
albumMock.create.mockResolvedValue(albumStub.empty);
await expect(sut.create(authStub.admin, { albumName: 'Empty album' })).resolves.toEqual({
albumName: 'Empty album',
albumThumbnailAssetId: null,
assetCount: 0,
assets: [],
createdAt: expect.anything(),
id: 'album-1',
owner: {
createdAt: '2021-01-01',
email: 'admin@test.com',
firstName: 'admin_first_name',
id: 'admin_id',
isAdmin: true,
lastName: 'admin_last_name',
oauthId: '',
profileImagePath: '',
shouldChangePassword: false,
storageLabel: 'admin',
updatedAt: '2021-01-01',
},
ownerId: 'admin_id',
shared: false,
sharedUsers: [],
updatedAt: expect.anything(),
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.SEARCH_INDEX_ALBUM,
data: { ids: [albumStub.empty.id] },
});
});
});
});

View file

@ -1,19 +1,22 @@
import { AlbumEntity } from '@app/infra/entities';
import { AlbumEntity, AssetEntity, UserEntity } from '@app/infra/entities';
import { 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 { AlbumResponseDto } from './response-dto';
import { AlbumResponseDto, mapAlbum } from './response-dto';
@Injectable()
export class AlbumService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {}
async getAllAlbums({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
async getAll({ id: ownerId }: AuthUserDto, { assetId, shared }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
await this.updateInvalidThumbnails();
let albums: AlbumEntity[];
@ -55,4 +58,17 @@ export class AlbumService {
return invalidAlbumIds.length;
}
async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
// TODO: Handle nonexistent sharedWithUserIds and assetIds.
const album = await this.albumRepository.create({
ownerId: authUser.id,
albumName: dto.albumName,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)),
albumThumbnailAssetId: dto.assetIds?.[0] || null,
});
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } });
return mapAlbum(album);
}
}

View file

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateAlbumDto {
@IsNotEmpty()
@IsString()
@ApiProperty()
albumName!: string;
@ValidateUUID({ optional: true, each: true })
sharedWithUserIds?: string[];
@ValidateUUID({ optional: true, each: true })
assetIds?: string[];
}

View file

@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { toBoolean } from 'apps/immich/src/utils/transform.util';
import { ApiProperty } from '@nestjs/swagger';
import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator';
import { toBoolean } from '../../../../../apps/immich/src/utils/transform.util';
export class GetAlbumsDto {
@IsOptional()

View file

@ -0,0 +1,2 @@
export * from './album-create.dto';
export * from './get-albums.dto';

View file

@ -1,3 +1,4 @@
export * from './album.repository';
export * from './album.service';
export * from './dto';
export * from './response-dto';

View file

@ -11,6 +11,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
getNotShared: jest.fn(),
deleteAll: jest.fn(),
getAll: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};
};

View file

@ -123,8 +123,12 @@ export class AlbumRepository implements IAlbumRepository {
});
}
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
return this.save(album);
}
async save(album: Partial<AlbumEntity>) {
const { id } = await this.repository.save(album);
return this.repository.findOneOrFail({ where: { id } });
return this.repository.findOneOrFail({ where: { id }, relations: { owner: true } });
}
}