From 4cc6e3b966cf03a81a03b7ed1c3129a400f54199 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 25 May 2023 15:37:19 -0400 Subject: [PATCH] refactor(server): update album (#2562) * refactor: update album * fix: remove unnecessary decorator --- .../src/api-v1/album/album-repository.ts | 2 +- .../src/api-v1/album/album.controller.ts | 11 +- .../src/api-v1/album/album.service.spec.ts | 40 +-- .../immich/src/api-v1/album/album.service.ts | 15 - .../src/controllers/album.controller.ts | 10 +- server/immich-openapi-specs.json | 299 +++++++++--------- .../libs/domain/src/album/album.repository.ts | 3 +- .../domain/src/album/album.service.spec.ts | 52 ++- server/libs/domain/src/album/album.service.ts | 35 +- .../domain/src/album/dto/album-update.dto.ts} | 2 +- server/libs/domain/src/album/dto/index.ts | 1 + .../libs/domain/test/album.repository.mock.ts | 3 +- .../src/repositories/album.repository.ts | 24 +- 13 files changed, 272 insertions(+), 225 deletions(-) rename server/{apps/immich/src/api-v1/album/dto/update-album.dto.ts => libs/domain/src/album/dto/album-update.dto.ts} (72%) diff --git a/server/apps/immich/src/api-v1/album/album-repository.ts b/server/apps/immich/src/api-v1/album/album-repository.ts index e349948011..070a4d31f9 100644 --- a/server/apps/immich/src/api-v1/album/album-repository.ts +++ b/server/apps/immich/src/api-v1/album/album-repository.ts @@ -6,7 +6,7 @@ import { Repository } from 'typeorm'; import { AddAssetsDto } from './dto/add-assets.dto'; import { AddUsersDto } from './dto/add-users.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 { AddAssetsResponseDto } from './response-dto/add-assets-response.dto'; diff --git a/server/apps/immich/src/api-v1/album/album.controller.ts b/server/apps/immich/src/api-v1/album/album.controller.ts index 846154f557..ee8214e995 100644 --- a/server/apps/immich/src/api-v1/album/album.controller.ts +++ b/server/apps/immich/src/api-v1/album/album.controller.ts @@ -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 { AlbumService } from './album.service'; 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 { AddUsersDto } from './dto/add-users.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto'; -import { UpdateAlbumDto } from './dto/update-album.dto'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { AlbumResponseDto } from '@app/domain'; import { AlbumCountResponseDto } from './response-dto/album-count-response.dto'; @@ -94,14 +93,6 @@ export class AlbumController { 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 }) @Get(':id/download') @ApiOkResponse({ content: { 'application/zip': { schema: { type: 'string', format: 'binary' } } } }) diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 6ecc675833..63d26178e7 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -2,7 +2,7 @@ import { AlbumService } from './album.service'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; 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 { IAlbumRepository } from './album-repository'; 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); }); - 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)); - 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)); - - 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 () => { const albumEntity = _getOwnedAlbum(); diff --git a/server/apps/immich/src/api-v1/album/album.service.ts b/server/apps/immich/src/api-v1/album/album.service.ts index 8f49e19a8a..78af9dc894 100644 --- a/server/apps/immich/src/api-v1/album/album.service.ts +++ b/server/apps/immich/src/api-v1/album/album.service.ts @@ -3,7 +3,6 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AlbumEntity, SharedLinkType } from '@app/infra/entities'; import { AddUsersDto } from './dto/add-users.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto'; -import { UpdateAlbumDto } from './dto/update-album.dto'; import { AlbumResponseDto, IJobRepository, JobName, mapAlbum } from '@app/domain'; import { IAlbumRepository } from './album-repository'; 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 { - 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 { return this.albumRepository.getCountByUserId(authUser.id); } diff --git a/server/apps/immich/src/controllers/album.controller.ts b/server/apps/immich/src/controllers/album.controller.ts index 5b883b3b64..b40d47544a 100644 --- a/server/apps/immich/src/controllers/album.controller.ts +++ b/server/apps/immich/src/controllers/album.controller.ts @@ -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 { 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 { GetAuthUser } from '../decorators/auth-user.decorator'; import { Authenticated } from '../decorators/authenticated.decorator'; import { UseValidation } from '../decorators/use-validation.decorator'; +import { UUIDParamDto } from './dto/uuid-param.dto'; @ApiTags('Album') @Controller('album') @@ -22,4 +23,9 @@ export class AlbumController { createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) { 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); + } } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 1c5ecdf1bd..d3d718606f 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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": { "post": { "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}": { "delete": { "operationId": "removeUserFromAlbum", @@ -4605,6 +4614,18 @@ "albumName" ] }, + "UpdateAlbumDto": { + "type": "object", + "properties": { + "albumName": { + "type": "string" + }, + "albumThumbnailAssetId": { + "type": "string", + "format": "uuid" + } + } + }, "APIKeyCreateDto": { "type": "object", "properties": { @@ -6372,18 +6393,6 @@ "alreadyInAlbum" ] }, - "UpdateAlbumDto": { - "type": "object", - "properties": { - "albumName": { - "type": "string" - }, - "albumThumbnailAssetId": { - "type": "string", - "format": "uuid" - } - } - }, "CreateAlbumShareLinkDto": { "type": "object", "properties": { diff --git a/server/libs/domain/src/album/album.repository.ts b/server/libs/domain/src/album/album.repository.ts index 55c7fa84fa..efdbcfa993 100644 --- a/server/libs/domain/src/album/album.repository.ts +++ b/server/libs/domain/src/album/album.repository.ts @@ -10,6 +10,7 @@ export interface AlbumAssetCount { export interface IAlbumRepository { getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; + hasAsset(id: string, assetId: string): Promise; getAssetCountForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; @@ -18,5 +19,5 @@ export interface IAlbumRepository { deleteAll(userId: string): Promise; getAll(): Promise; create(album: Partial): Promise; - save(album: Partial): Promise; + update(album: Partial): Promise; } diff --git a/server/libs/domain/src/album/album.service.spec.ts b/server/libs/domain/src/album/album.service.spec.ts index 4e902241ab..18c12c1a33 100644 --- a/server/libs/domain/src/album/album.service.spec.ts +++ b/server/libs/domain/src/album/album.service.spec.ts @@ -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] }, + }); + }); + }); }); diff --git a/server/libs/domain/src/album/album.service.ts b/server/libs/domain/src/album/album.service.ts index a368fbecca..d56c576d3e 100644 --- a/server/libs/domain/src/album/album.service.ts +++ b/server/libs/domain/src/album/album.service.ts @@ -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 { + 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); + } } diff --git a/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts b/server/libs/domain/src/album/dto/album-update.dto.ts similarity index 72% rename from server/apps/immich/src/api-v1/album/dto/update-album.dto.ts rename to server/libs/domain/src/album/dto/album-update.dto.ts index 50853cd5aa..e71722b74f 100644 --- a/server/apps/immich/src/api-v1/album/dto/update-album.dto.ts +++ b/server/libs/domain/src/album/dto/album-update.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ValidateUUID } from 'apps/immich/src/decorators/validate-uuid.decorator'; import { IsOptional } from 'class-validator'; +import { ValidateUUID } from '../../../../../apps/immich/src/decorators/validate-uuid.decorator'; export class UpdateAlbumDto { @IsOptional() diff --git a/server/libs/domain/src/album/dto/index.ts b/server/libs/domain/src/album/dto/index.ts index 2a7e8df243..5481afb175 100644 --- a/server/libs/domain/src/album/dto/index.ts +++ b/server/libs/domain/src/album/dto/index.ts @@ -1,2 +1,3 @@ export * from './album-create.dto'; +export * from './album-update.dto'; export * from './get-albums.dto'; diff --git a/server/libs/domain/test/album.repository.mock.ts b/server/libs/domain/test/album.repository.mock.ts index 74ddd8aec4..944e062257 100644 --- a/server/libs/domain/test/album.repository.mock.ts +++ b/server/libs/domain/test/album.repository.mock.ts @@ -11,7 +11,8 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { getNotShared: jest.fn(), deleteAll: jest.fn(), getAll: jest.fn(), + hasAsset: jest.fn(), create: jest.fn(), - save: jest.fn(), + update: jest.fn(), }; }; diff --git a/server/libs/infra/src/repositories/album.repository.ts b/server/libs/infra/src/repositories/album.repository.ts index cab574ed48..9d8aef7b4b 100644 --- a/server/libs/infra/src/repositories/album.repository.ts +++ b/server/libs/infra/src/repositories/album.repository.ts @@ -123,11 +123,31 @@ export class AlbumRepository implements IAlbumRepository { }); } - create(album: Partial): Promise { + async hasAsset(id: string, assetId: string): Promise { + const count = await this.repository.count({ + where: { + id, + assets: { + id: assetId, + }, + }, + relations: { + assets: true, + }, + }); + + return Boolean(count); + } + + async create(album: Partial): Promise { return this.save(album); } - async save(album: Partial) { + async update(album: Partial) { + return this.save(album); + } + + private async save(album: Partial) { const { id } = await this.repository.save(album); return this.repository.findOneOrFail({ where: { id }, relations: { owner: true } }); }