mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
refactor: create album (#2555)
This commit is contained in:
parent
83df14d379
commit
d827a6182b
16 changed files with 117 additions and 91 deletions
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
server/libs/domain/src/album/dto/album-create.dto.ts
Normal file
16
server/libs/domain/src/album/dto/album-create.dto.ts
Normal 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[];
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
2
server/libs/domain/src/album/dto/index.ts
Normal file
2
server/libs/domain/src/album/dto/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './album-create.dto';
|
||||
export * from './get-albums.dto';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './album.repository';
|
||||
export * from './album.service';
|
||||
export * from './dto';
|
||||
export * from './response-dto';
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue