mirror of
https://github.com/immich-app/immich.git
synced 2025-01-06 11:56:46 +01:00
refactor(server): update album (#2562)
* refactor: update album * fix: remove unnecessary decorator
This commit is contained in:
parent
1c293a2759
commit
4cc6e3b966
13 changed files with 272 additions and 225 deletions
|
@ -6,7 +6,7 @@ import { Repository } from 'typeorm';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
import { UpdateAlbumDto } from '@app/domain';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Put, Query, Response } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Param, Delete, Put, Query, Response } from '@nestjs/common';
|
||||||
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
|
import { ParseMeUUIDPipe } from '../validation/parse-me-uuid-pipe';
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from './album.service';
|
||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
|
@ -6,7 +6,6 @@ import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
|
||||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { AlbumResponseDto } from '@app/domain';
|
import { AlbumResponseDto } from '@app/domain';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
@ -94,14 +93,6 @@ export class AlbumController {
|
||||||
return this.service.removeUser(authUser, id, userId);
|
return this.service.removeUser(authUser, id, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated()
|
|
||||||
@Patch(':id')
|
|
||||||
updateAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
|
|
||||||
// TODO: Handle nonexistent albumThumbnailAssetId.
|
|
||||||
// TODO: Disallow setting asset from other user as albumThumbnailAssetId.
|
|
||||||
return this.service.update(authUser, id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
@Authenticated({ isShared: true })
|
||||||
@Get(':id/download')
|
@Get(':id/download')
|
||||||
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
|
@ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } })
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { AlbumService } from './album.service';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
import { AlbumEntity, UserEntity } from '@app/infra/entities';
|
import { AlbumEntity, UserEntity } from '@app/infra/entities';
|
||||||
import { AlbumResponseDto, ICryptoRepository, IJobRepository, JobName, mapUser } from '@app/domain';
|
import { AlbumResponseDto, ICryptoRepository, IJobRepository, mapUser } from '@app/domain';
|
||||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
|
@ -259,44 +259,6 @@ describe('Album service', () => {
|
||||||
await expect(sut.removeUser(authUser, albumEntity.id, authUser.id)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.removeUser(authUser, albumEntity.id, authUser.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates a owned album', async () => {
|
|
||||||
const albumEntity = _getOwnedAlbum();
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
const updatedAlbumName = 'new album name';
|
|
||||||
const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
const updatedAlbum = { ...albumEntity, albumName: updatedAlbumName };
|
|
||||||
albumRepositoryMock.updateAlbum.mockResolvedValue(updatedAlbum);
|
|
||||||
|
|
||||||
const result = await sut.update(authUser, albumId, {
|
|
||||||
albumName: updatedAlbumName,
|
|
||||||
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.id).toEqual(albumId);
|
|
||||||
expect(result.albumName).toEqual(updatedAlbumName);
|
|
||||||
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
|
|
||||||
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
|
|
||||||
albumName: updatedAlbumName,
|
|
||||||
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
|
|
||||||
});
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prevents updating a not owned album (shared with auth user)', async () => {
|
|
||||||
const albumEntity = _getSharedWithAuthUserAlbum();
|
|
||||||
const albumId = albumEntity.id;
|
|
||||||
|
|
||||||
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.update(authUser, albumId, {
|
|
||||||
albumName: 'new album name',
|
|
||||||
albumThumbnailAssetId: '69d2f917-0b31-48d8-9d7d-673b523f1aac',
|
|
||||||
}),
|
|
||||||
).rejects.toBeInstanceOf(ForbiddenException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds assets to owned album', async () => {
|
it('adds assets to owned album', async () => {
|
||||||
const albumEntity = _getOwnedAlbum();
|
const albumEntity = _getOwnedAlbum();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { AlbumEntity, SharedLinkType } from '@app/infra/entities';
|
import { AlbumEntity, SharedLinkType } from '@app/infra/entities';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
|
||||||
import { AlbumResponseDto, IJobRepository, JobName, mapAlbum } from '@app/domain';
|
import { AlbumResponseDto, IJobRepository, JobName, mapAlbum } from '@app/domain';
|
||||||
import { IAlbumRepository } from './album-repository';
|
import { IAlbumRepository } from './album-repository';
|
||||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||||
|
@ -116,20 +115,6 @@ export class AlbumService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(authUser: AuthUserDto, albumId: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
|
|
||||||
const album = await this._getAlbum({ authUser, albumId });
|
|
||||||
|
|
||||||
if (authUser.id != album.ownerId) {
|
|
||||||
throw new BadRequestException('Unauthorized to change album info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAlbum = await this.albumRepository.updateAlbum(album, dto);
|
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
|
||||||
|
|
||||||
return mapAlbum(updatedAlbum);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
async getCountByUserId(authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||||
return this.albumRepository.getCountByUserId(authUser.id);
|
return this.albumRepository.getCountByUserId(authUser.id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { AlbumService, AuthUserDto, CreateAlbumDto } from '@app/domain';
|
/* */ import { AlbumService, AuthUserDto, CreateAlbumDto, UpdateAlbumDto } from '@app/domain';
|
||||||
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
|
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
|
||||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
import { GetAuthUser } from '../decorators/auth-user.decorator';
|
||||||
import { Authenticated } from '../decorators/authenticated.decorator';
|
import { Authenticated } from '../decorators/authenticated.decorator';
|
||||||
import { UseValidation } from '../decorators/use-validation.decorator';
|
import { UseValidation } from '../decorators/use-validation.decorator';
|
||||||
|
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||||
|
|
||||||
@ApiTags('Album')
|
@ApiTags('Album')
|
||||||
@Controller('album')
|
@Controller('album')
|
||||||
|
@ -22,4 +23,9 @@ export class AlbumController {
|
||||||
createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) {
|
createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) {
|
||||||
return this.service.create(authUser, dto);
|
return this.service.create(authUser, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
updateAlbumInfo(@GetAuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) {
|
||||||
|
return this.service.update(authUser, id, dto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,148 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/album/{id}": {
|
||||||
|
"patch": {
|
||||||
|
"operationId": "updateAlbumInfo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UpdateAlbumDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AlbumResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Album"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAlbumInfo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AlbumResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Album"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteAlbum",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Album"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api-key": {
|
"/api-key": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "createKey",
|
"operationId": "createKey",
|
||||||
|
@ -3859,139 +4001,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/album/{id}": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getAlbumInfo",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AlbumResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Album"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"operationId": "deleteAlbum",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Album"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"patch": {
|
|
||||||
"operationId": "updateAlbumInfo",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UpdateAlbumDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AlbumResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"Album"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/album/{id}/user/{userId}": {
|
"/album/{id}/user/{userId}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "removeUserFromAlbum",
|
"operationId": "removeUserFromAlbum",
|
||||||
|
@ -4605,6 +4614,18 @@
|
||||||
"albumName"
|
"albumName"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"UpdateAlbumDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"albumName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"albumThumbnailAssetId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"APIKeyCreateDto": {
|
"APIKeyCreateDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -6372,18 +6393,6 @@
|
||||||
"alreadyInAlbum"
|
"alreadyInAlbum"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"UpdateAlbumDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"albumName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"albumThumbnailAssetId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "uuid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CreateAlbumShareLinkDto": {
|
"CreateAlbumShareLinkDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -10,6 +10,7 @@ export interface AlbumAssetCount {
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository {
|
||||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||||
|
hasAsset(id: string, assetId: string): Promise<boolean>;
|
||||||
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||||
getInvalidThumbnail(): Promise<string[]>;
|
getInvalidThumbnail(): Promise<string[]>;
|
||||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||||
|
@ -18,5 +19,5 @@ export interface IAlbumRepository {
|
||||||
deleteAll(userId: string): Promise<void>;
|
deleteAll(userId: string): Promise<void>;
|
||||||
getAll(): Promise<AlbumEntity[]>;
|
getAll(): Promise<AlbumEntity[]>;
|
||||||
create(album: Partial<AlbumEntity>): 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 { albumStub, authStub, newAlbumRepositoryMock, newAssetRepositoryMock, newJobRepositoryMock } from '../../test';
|
||||||
import { IAssetRepository } from '../asset';
|
import { IAssetRepository } from '../asset';
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
|
@ -89,14 +90,14 @@ describe(AlbumService.name, () => {
|
||||||
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
|
{ albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
|
||||||
]);
|
]);
|
||||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
|
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
|
||||||
albumMock.save.mockResolvedValue(albumStub.oneAssetValidThumbnail);
|
albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
|
||||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
|
assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
|
||||||
|
|
||||||
const result = await sut.getAll(authStub.admin, {});
|
const result = await sut.getAll(authStub.admin, {});
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(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 () => {
|
it('removes the thumbnail for an empty album', async () => {
|
||||||
|
@ -105,14 +106,14 @@ describe(AlbumService.name, () => {
|
||||||
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
|
{ albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
|
||||||
]);
|
]);
|
||||||
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
|
albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
|
||||||
albumMock.save.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
|
||||||
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
|
assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
|
||||||
|
|
||||||
const result = await sut.getAll(authStub.admin, {});
|
const result = await sut.getAll(authStub.admin, {});
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1);
|
||||||
expect(albumMock.save).toHaveBeenCalledTimes(1);
|
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
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 { 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 { IAssetRepository } from '../asset';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IAlbumRepository } from './album.repository';
|
import { IAlbumRepository } from './album.repository';
|
||||||
import { CreateAlbumDto } from './dto/album-create.dto';
|
import { CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto } from './dto';
|
||||||
import { GetAlbumsDto } from './dto/get-albums.dto';
|
|
||||||
import { AlbumResponseDto, mapAlbum } from './response-dto';
|
import { AlbumResponseDto, mapAlbum } from './response-dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -53,7 +52,7 @@ export class AlbumService {
|
||||||
|
|
||||||
for (const albumId of invalidAlbumIds) {
|
for (const albumId of invalidAlbumIds) {
|
||||||
const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId);
|
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;
|
return invalidAlbumIds.length;
|
||||||
|
@ -71,4 +70,32 @@ export class AlbumService {
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [album.id] } });
|
||||||
return mapAlbum(album);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator';
|
|
||||||
import { IsOptional } from 'class-validator';
|
import { IsOptional } from 'class-validator';
|
||||||
|
import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator';
|
||||||
|
|
||||||
export class UpdateAlbumDto {
|
export class UpdateAlbumDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './album-create.dto';
|
export * from './album-create.dto';
|
||||||
|
export * from './album-update.dto';
|
||||||
export * from './get-albums.dto';
|
export * from './get-albums.dto';
|
||||||
|
|
|
@ -11,7 +11,8 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
||||||
getNotShared: jest.fn(),
|
getNotShared: jest.fn(),
|
||||||
deleteAll: jest.fn(),
|
deleteAll: jest.fn(),
|
||||||
getAll: jest.fn(),
|
getAll: jest.fn(),
|
||||||
|
hasAsset: jest.fn(),
|
||||||
create: 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);
|
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);
|
const { id } = await this.repository.save(album);
|
||||||
return this.repository.findOneOrFail({ where: { id }, relations: { owner: true } });
|
return this.repository.findOneOrFail({ where: { id }, relations: { owner: true } });
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue