From 4ea281f854e7e51d2348851629e10000451ad49d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 2 Oct 2024 10:54:35 -0400 Subject: [PATCH] refactor: service dependencies (#13108) refactor(server): simplify service dependency management --- server/src/services/activity.service.spec.ts | 10 +- server/src/services/activity.service.ts | 30 ++-- server/src/services/album.service.spec.ts | 22 +-- server/src/services/album.service.ts | 43 ++--- server/src/services/api-key.service.spec.ts | 10 +- server/src/services/api-key.service.ts | 30 ++-- .../src/services/asset-media.service.spec.ts | 24 +-- server/src/services/asset-media.service.ts | 41 +---- server/src/services/asset.service.spec.ts | 50 +----- server/src/services/asset.service.ts | 38 +---- server/src/services/audit.service.spec.ts | 32 +--- server/src/services/audit.service.ts | 32 +--- server/src/services/auth.service.spec.ts | 60 ++----- server/src/services/auth.service.ts | 27 +-- server/src/services/base.service.ts | 85 +++++++++- server/src/services/cli.service.spec.ts | 22 +-- server/src/services/cli.service.ts | 18 +- server/src/services/database.service.spec.ts | 12 +- server/src/services/database.service.ts | 16 +- server/src/services/download.service.spec.ts | 15 +- server/src/services/download.service.ts | 27 +-- server/src/services/duplicate.service.spec.ts | 29 +--- server/src/services/duplicate.service.ts | 32 +--- server/src/services/job.service.spec.ts | 29 +--- server/src/services/job.service.ts | 25 +-- server/src/services/library.service.spec.ts | 39 +---- server/src/services/library.service.ts | 55 ++---- server/src/services/map.service.spec.ts | 15 +- server/src/services/map.service.ts | 13 +- server/src/services/media.service.spec.ts | 45 +---- server/src/services/media.service.ts | 47 +---- server/src/services/memory.service.spec.ts | 12 +- server/src/services/memory.service.ts | 42 ++--- server/src/services/metadata.service.spec.ts | 79 ++------- server/src/services/metadata.service.ts | 71 ++------ .../src/services/notification.service.spec.ts | 40 +---- server/src/services/notification.service.ts | 28 +-- server/src/services/partner.service.spec.ts | 7 +- server/src/services/partner.service.ts | 27 ++- server/src/services/person.service.spec.ts | 60 ++----- server/src/services/person.service.ts | 146 ++++++---------- server/src/services/search.service.spec.ts | 40 +---- server/src/services/search.service.ts | 31 +--- server/src/services/server.service.spec.ts | 29 +--- server/src/services/server.service.ts | 23 +-- server/src/services/session.service.spec.ts | 14 +- server/src/services/session.service.ts | 18 +- .../src/services/shared-link.service.spec.ts | 118 ++++++------- server/src/services/shared-link.service.ts | 40 ++--- .../src/services/smart-info.service.spec.ts | 50 ++---- server/src/services/smart-info.service.ts | 38 +---- server/src/services/stack.service.ts | 24 +-- .../services/storage-template.service.spec.ts | 48 +----- .../src/services/storage-template.service.ts | 42 +---- server/src/services/storage.service.spec.ts | 20 +-- server/src/services/storage.service.ts | 25 +-- server/src/services/sync.service.spec.ts | 16 +- server/src/services/sync.service.ts | 19 +-- .../services/system-config.service.spec.ts | 16 +- server/src/services/system-config.service.ts | 17 +- .../services/system-metadata.service.spec.ts | 11 +- .../src/services/system-metadata.service.ts | 14 +- server/src/services/tag.service.spec.ts | 13 +- server/src/services/tag.service.ts | 55 +++--- server/src/services/timeline.service.spec.ts | 15 +- server/src/services/timeline.service.ts | 28 ++- server/src/services/trash.service.spec.ts | 20 +-- server/src/services/trash.service.ts | 22 +-- .../src/services/user-admin.service.spec.ts | 25 +-- server/src/services/user-admin.service.ts | 24 +-- server/src/services/user.service.spec.ts | 37 +--- server/src/services/user.service.ts | 26 +-- server/src/services/version.service.spec.ts | 54 ++---- server/src/services/version.service.ts | 30 +--- server/src/services/view.service.spec.ts | 6 +- server/src/services/view.service.ts | 7 +- server/test/utils.ts | 160 ++++++++++++++++++ 77 files changed, 800 insertions(+), 1860 deletions(-) create mode 100644 server/test/utils.ts diff --git a/server/src/services/activity.service.spec.ts b/server/src/services/activity.service.spec.ts index 30720b6c1fb29..f9a8e6ce47bc6 100644 --- a/server/src/services/activity.service.spec.ts +++ b/server/src/services/activity.service.spec.ts @@ -4,20 +4,18 @@ import { IActivityRepository } from 'src/interfaces/activity.interface'; import { ActivityService } from 'src/services/activity.service'; import { activityStub } from 'test/fixtures/activity.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(ActivityService.name, () => { let sut: ActivityService; + let accessMock: IAccessRepositoryMock; let activityMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - activityMock = newActivityRepositoryMock(); - - sut = new ActivityService(accessMock, activityMock); + ({ sut, accessMock, activityMock } = newTestService(ActivityService)); }); it('should work', () => { diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 1e4034de936fa..4e17baebc32e5 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ActivityCreateDto, ActivityDto, @@ -13,20 +13,14 @@ import { import { AuthDto } from 'src/dtos/auth.dto'; import { ActivityEntity } from 'src/entities/activity.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; @Injectable() -export class ActivityService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IActivityRepository) private repository: IActivityRepository, - ) {} - +export class ActivityService extends BaseService { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); - const activities = await this.repository.search({ + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + const activities = await this.activityRepository.search({ userId: dto.userId, albumId: dto.albumId, assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId, @@ -37,12 +31,12 @@ export class ActivityService { } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); - return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) }; } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { - await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); const common = { userId: auth.user.id, @@ -55,7 +49,7 @@ export class ActivityService { if (dto.type === ReactionType.LIKE) { delete dto.comment; - [activity] = await this.repository.search({ + [activity] = await this.activityRepository.search({ ...common, // `null` will search for an album like assetId: dto.assetId ?? null, @@ -65,7 +59,7 @@ export class ActivityService { } if (!activity) { - activity = await this.repository.create({ + activity = await this.activityRepository.create({ ...common, isLiked: dto.type === ReactionType.LIKE, comment: dto.comment, @@ -76,7 +70,7 @@ export class ActivityService { } async delete(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); - await this.repository.delete(id); + await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); + await this.activityRepository.delete(id); } } diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index b8624b29aebd5..dbfd8e869551e 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -4,39 +4,27 @@ import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AlbumUserRole } from 'src/enum'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AlbumService } from 'src/services/album.service'; import { albumStub } from 'test/fixtures/album.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(AlbumService.name, () => { let sut: AlbumService; + let accessMock: IAccessRepositoryMock; let albumMock: Mocked; - let assetMock: Mocked; + let albumUserMock: Mocked; let eventMock: Mocked; let userMock: Mocked; - let albumUserMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - albumMock = newAlbumRepositoryMock(); - assetMock = newAssetRepositoryMock(); - eventMock = newEventRepositoryMock(); - userMock = newUserRepositoryMock(); - albumUserMock = newAlbumUserRepositoryMock(); - - sut = new AlbumService(accessMock, albumMock, assetMock, eventMock, userMock, albumUserMock); + ({ sut, accessMock, albumMock, albumUserMock, eventMock, userMock } = newTestService(AlbumService)); }); it('should work', () => { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 2f5d2308415ff..a9a678d605091 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { AddUsersDto, AlbumInfoDto, @@ -17,26 +17,13 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; -import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; +import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface'; +import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() -export class AlbumService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, - ) {} - +export class AlbumService extends BaseService { async getStatistics(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ this.albumRepository.getOwned(auth.user.id), @@ -95,7 +82,7 @@ export class AlbumService { } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [id] }); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -119,7 +106,7 @@ export class AlbumService { } } - const allowedAssetIdsSet = await checkAccess(this.access, { + const allowedAssetIdsSet = await checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds || [], @@ -143,7 +130,7 @@ export class AlbumService { } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: true }); @@ -166,17 +153,17 @@ export class AlbumService { } async delete(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DELETE, ids: [id] }); await this.albumRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); const results = await addAssets( auth, - { access: this.access, bulk: this.albumRepository }, + { access: this.accessRepository, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids }, ); @@ -195,12 +182,12 @@ export class AlbumService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( auth, - { access: this.access, bulk: this.albumRepository }, + { access: this.accessRepository, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, ); @@ -216,7 +203,7 @@ export class AlbumService { } async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); @@ -260,14 +247,14 @@ export class AlbumService { // non-admin can remove themselves if (auth.user.id !== userId) { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } await this.albumUserRepository.delete({ albumId: id, userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 4d13eead575fc..c0a24c1aafcc9 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -5,19 +5,17 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { APIKeyService } from 'src/services/api-key.service'; import { keyStub } from 'test/fixtures/api-key.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(APIKeyService.name, () => { let sut: APIKeyService; - let keyMock: Mocked; + let cryptoMock: Mocked; + let keyMock: Mocked; beforeEach(() => { - cryptoMock = newCryptoRepositoryMock(); - keyMock = newKeyRepositoryMock(); - sut = new APIKeyService(cryptoMock, keyMock); + ({ sut, cryptoMock, keyMock } = newTestService(APIKeyService)); }); describe('create', () => { diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 7dd1ed5c268ba..303ca05537781 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,27 +1,21 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { APIKeyEntity } from 'src/entities/api-key.entity'; -import { IKeyRepository } from 'src/interfaces/api-key.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { BaseService } from 'src/services/base.service'; import { isGranted } from 'src/utils/access'; @Injectable() -export class APIKeyService { - constructor( - @Inject(ICryptoRepository) private crypto: ICryptoRepository, - @Inject(IKeyRepository) private repository: IKeyRepository, - ) {} - +export class APIKeyService extends BaseService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { - const secret = this.crypto.newPassword(32); + const secret = this.cryptoRepository.newPassword(32); if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { throw new BadRequestException('Cannot grant permissions you do not have'); } - const entity = await this.repository.create({ - key: this.crypto.hashSha256(secret), + const entity = await this.keyRepository.create({ + key: this.cryptoRepository.hashSha256(secret), name: dto.name || 'API Key', userId: auth.user.id, permissions: dto.permissions, @@ -31,27 +25,27 @@ export class APIKeyService { } async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { - const exists = await this.repository.getById(auth.user.id, id); + const exists = await this.keyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - const key = await this.repository.update(auth.user.id, id, { name: dto.name }); + const key = await this.keyRepository.update(auth.user.id, id, { name: dto.name }); return this.map(key); } async delete(auth: AuthDto, id: string): Promise { - const exists = await this.repository.getById(auth.user.id, id); + const exists = await this.keyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - await this.repository.delete(auth.user.id, id); + await this.keyRepository.delete(auth.user.id, id); } async getById(auth: AuthDto, id: string): Promise { - const key = await this.repository.getById(auth.user.id, id); + const key = await this.keyRepository.getById(auth.user.id, id); if (!key) { throw new BadRequestException('API Key not found'); } @@ -59,7 +53,7 @@ export class APIKeyService { } async getAll(auth: AuthDto): Promise { - const keys = await this.repository.getByUserId(auth.user.id); + const keys = await this.keyRepository.getByUserId(auth.user.id); return keys.map((key) => this.map(key)); } diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index c03c974b2c8e2..3b067eb3ceda8 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -6,9 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, CacheControl } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AssetMediaService } from 'src/services/asset-media.service'; @@ -16,13 +14,8 @@ import { ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { QueryFailedError } from 'typeorm'; import { Mocked } from 'vitest'; @@ -189,24 +182,15 @@ const copiedAsset = Object.freeze({ describe(AssetMediaService.name, () => { let sut: AssetMediaService; + let accessMock: IAccessRepositoryMock; let assetMock: Mocked; let jobMock: Mocked; - let loggerMock: Mocked; let storageMock: Mocked; let userMock: Mocked; - let eventMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - jobMock = newJobRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - storageMock = newStorageRepositoryMock(); - userMock = newUserRepositoryMock(); - eventMock = newEventRepositoryMock(); - - sut = new AssetMediaService(accessMock, assetMock, jobMock, storageMock, userMock, eventMock, loggerMock); + ({ sut, accessMock, assetMock, jobMock, storageMock, userMock } = newTestService(AssetMediaService)); }); describe('getUploadAssetIdByChecksum', () => { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index e1b30e891f936..60234b51ef86e 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -1,10 +1,4 @@ -import { - BadRequestException, - Inject, - Injectable, - InternalServerErrorException, - NotFoundException, -} from '@nestjs/common'; +import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; @@ -28,13 +22,8 @@ import { import { AuthDto } from 'src/dtos/auth.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; +import { JobName } from 'src/interfaces/job.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess, requireUploadAccess } from 'src/utils/access'; import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; @@ -56,19 +45,7 @@ export interface UploadFile { } @Injectable() -export class AssetMediaService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - ) { - this.logger.setContext(AssetMediaService.name); - } - +export class AssetMediaService extends BaseService { async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise { if (!checksum) { return; @@ -148,7 +125,7 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await requireAccess(this.access, { + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPLOAD, // do not need an id here, but the interface requires it @@ -182,7 +159,7 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const asset = (await this.assetRepository.getById(id)) as AssetEntity; this.requireQuota(auth, file.size); @@ -205,7 +182,7 @@ export class AssetMediaService { } async downloadOriginal(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { @@ -220,7 +197,7 @@ export class AssetMediaService { } async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; @@ -243,7 +220,7 @@ export class AssetMediaService { } async playbackVideo(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 968b774b770d6..f8e5ff347e548 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -4,13 +4,10 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, AssetType } from 'src/enum'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AssetService } from 'src/services/asset.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -18,16 +15,8 @@ import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; -import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, vitest } from 'vitest'; const stats: AssetStats = { @@ -45,16 +34,14 @@ const statResponse: AssetStatsResponseDto = { describe(AssetService.name, () => { let sut: AssetService; + let accessMock: IAccessRepositoryMock; let assetMock: Mocked; - let configMock: Mocked; - let jobMock: Mocked; - let userMock: Mocked; let eventMock: Mocked; - let stackMock: Mocked; - let systemMock: Mocked; + let jobMock: Mocked; let partnerMock: Mocked; - let loggerMock: Mocked; + let stackMock: Mocked; + let userMock: Mocked; it('should work', () => { expect(sut).toBeDefined(); @@ -67,29 +54,8 @@ describe(AssetService.name, () => { }; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - userMock = newUserRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - partnerMock = newPartnerRepositoryMock(); - stackMock = newStackRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new AssetService( - accessMock, - assetMock, - configMock, - jobMock, - systemMock, - userMock, - eventMock, - partnerMock, - stackMock, - loggerMock, - ); + ({ sut, accessMock, assetMock, eventMock, jobMock, userMock, partnerMock, stackMock } = + newTestService(AssetService)); mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 11416880280c6..47ceba5c9f4de 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; import { @@ -20,46 +20,20 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetStatus, Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { IAssetDeleteJob, - IJobRepository, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobItem, JobName, JobStatus, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; -import { IStackRepository } from 'src/interfaces/stack.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService extends BaseService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(IStackRepository) private stackRepository: IStackRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(AssetService.name); - } - async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise { const partnerIds = await getMyPartnerIds({ userId: auth.user.id, @@ -112,7 +86,7 @@ export class AssetService extends BaseService { } async get(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [id] }); const asset = await this.assetRepository.getById( id, @@ -161,7 +135,7 @@ export class AssetService extends BaseService { } async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const repos = { asset: this.assetRepository, event: this.eventRepository }; @@ -204,7 +178,7 @@ export class AssetService extends BaseService { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; - await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids }); for (const id of ids) { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); @@ -301,7 +275,7 @@ export class AssetService extends BaseService { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids }); await this.assetRepository.updateAll(ids, { deletedAt: new Date(), status: force ? AssetStatus.DELETED : AssetStatus.TRASHED, @@ -310,7 +284,7 @@ export class AssetService extends BaseService { } async run(auth: AuthDto, dto: AssetJobsDto) { - await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const jobs: JobItem[] = []; diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index ef685f4a87755..20bbf5c739ab6 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,46 +1,18 @@ import { DatabaseAction, EntityType } from 'src/enum'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; import { AuditService } from 'src/services/audit.service'; import { auditStub } from 'test/fixtures/audit.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(AuditService.name, () => { let sut: AuditService; - let accessMock: IAccessRepositoryMock; - let assetMock: Mocked; let auditMock: Mocked; - let cryptoMock: Mocked; - let personMock: Mocked; - let storageMock: Mocked; - let userMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - auditMock = newAuditRepositoryMock(); - personMock = newPersonRepositoryMock(); - storageMock = newStorageRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - sut = new AuditService(accessMock, assetMock, cryptoMock, personMock, auditMock, storageMock, userMock, loggerMock); + ({ sut, auditMock } = newTestService(AuditService)); }); it('should work', () => { diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index ced0f49c63716..60f8d6fa81617 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; @@ -21,44 +21,24 @@ import { StorageFolder, UserPathType, } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IAuditRepository } from 'src/interfaces/audit.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class AuditService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(IAuditRepository) private repository: IAuditRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - ) { - this.logger.setContext(AuditService.name); - } - +export class AuditService extends BaseService { async handleCleanup(): Promise { - await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); + await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); return JobStatus.SUCCESS; } async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { const userId = dto.userId || auth.user.id; - await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); - const audits = await this.repository.getAfter(dto.after, { + const audits = await this.auditRepository.getAfter(dto.after, { userIds: [userId], entityType: dto.entityType, action: DatabaseAction.DELETE, diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 7cb79b80a1905..6ab256db7b552 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -5,10 +5,8 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { AuthType } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -20,15 +18,7 @@ import { sessionStub } from 'test/fixtures/session.stub'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; -import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mock, Mocked, vitest } from 'vitest'; // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); @@ -59,15 +49,14 @@ const oauthUserWithDefaultQuota = { describe('AuthService', () => { let sut: AuthService; - let configMock: Mocked; + let cryptoMock: Mocked; let eventMock: Mocked; - let userMock: Mocked; - let loggerMock: Mocked; - let systemMock: Mocked; - let sessionMock: Mocked; - let shareMock: Mocked; let keyMock: Mocked; + let sessionMock: Mocked; + let sharedLinkMock: Mocked; + let systemMock: Mocked; + let userMock: Mocked; let callbackMock: Mock; let userinfoMock: Mock; @@ -92,27 +81,8 @@ describe('AuthService', () => { }), } as any); - configMock = newConfigRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - eventMock = newEventRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - sessionMock = newSessionRepositoryMock(); - shareMock = newSharedLinkRepositoryMock(); - keyMock = newKeyRepositoryMock(); - - sut = new AuthService( - configMock, - cryptoMock, - eventMock, - systemMock, - loggerMock, - userMock, - sessionMock, - shareMock, - keyMock, - ); + ({ sut, cryptoMock, eventMock, keyMock, sessionMock, sharedLinkMock, systemMock, userMock } = + newTestService(AuthService)); }); it('should be defined', () => { @@ -297,7 +267,7 @@ describe('AuthService', () => { describe('validate - shared key', () => { it('should not accept a non-existent key', async () => { - shareMock.getByKey.mockResolvedValue(null); + sharedLinkMock.getByKey.mockResolvedValue(null); await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -308,7 +278,7 @@ describe('AuthService', () => { }); it('should not accept an expired key', async () => { - shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); + sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired); await expect( sut.authenticate({ headers: { 'x-immich-share-key': 'key' }, @@ -319,7 +289,7 @@ describe('AuthService', () => { }); it('should not accept a key without a user', async () => { - shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); + sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired); userMock.get.mockResolvedValue(null); await expect( sut.authenticate({ @@ -331,7 +301,7 @@ describe('AuthService', () => { }); it('should accept a base64url key', async () => { - shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); await expect( sut.authenticate({ @@ -343,11 +313,11 @@ describe('AuthService', () => { user: userStub.admin, sharedLink: sharedLinkStub.valid, }); - expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); + expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); }); it('should accept a hex key', async () => { - shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); await expect( sut.authenticate({ @@ -359,7 +329,7 @@ describe('AuthService', () => { user: userStub.admin, sharedLink: sharedLinkStub.valid, }); - expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); + expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 3e4a55b7ff044..ba1d599885a10 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, ForbiddenException, - Inject, Injectable, InternalServerErrorException, UnauthorizedException, @@ -13,6 +12,7 @@ import { IncomingHttpHeaders } from 'node:http'; import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { SystemConfig } from 'src/config'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; +import { OnEvent } from 'src/decorators'; import { AuthDto, ChangePasswordDto, @@ -30,15 +30,6 @@ import { import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AuthType, Permission } from 'src/enum'; -import { IKeyRepository } from 'src/interfaces/api-key.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISessionRepository } from 'src/interfaces/session.interface'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; @@ -72,20 +63,8 @@ export type ValidateRequest = { @Injectable() export class AuthService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ISessionRepository) private sessionRepository: ISessionRepository, - @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, - @Inject(IKeyRepository) private keyRepository: IKeyRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(AuthService.name); - + @OnEvent({ name: 'app.bootstrap' }) + onBootstrap() { custom.setHttpOptionsDefaults({ timeout: 30_000 }); } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index a2ddcb1e5000a..3c28451a6d3c9 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -1,16 +1,97 @@ import { Inject } from '@nestjs/common'; import { SystemConfig } from 'src/config'; +import { StorageCore } from 'src/cores/storage.core'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; +import { IAlbumRepository } from 'src/interfaces/album.interface'; +import { IKeyRepository } from 'src/interfaces/api-key.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IConfigRepository } from 'src/interfaces/config.interface'; +import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository } from 'src/interfaces/job.interface'; +import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; +import { IMapRepository } from 'src/interfaces/map.interface'; +import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { IMetadataRepository } from 'src/interfaces/metadata.interface'; +import { IMetricRepository } from 'src/interfaces/metric.interface'; +import { IMoveRepository } from 'src/interfaces/move.interface'; +import { INotificationRepository } from 'src/interfaces/notification.interface'; +import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { IPersonRepository } from 'src/interfaces/person.interface'; +import { ISearchRepository } from 'src/interfaces/search.interface'; +import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; +import { ISessionRepository } from 'src/interfaces/session.interface'; +import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; +import { IStackRepository } from 'src/interfaces/stack.interface'; +import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; +import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { IUserRepository } from 'src/interfaces/user.interface'; +import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; +import { IViewRepository } from 'src/interfaces/view.interface'; import { getConfig, updateConfig } from 'src/utils/config'; export class BaseService { + protected storageCore: StorageCore; + constructor( + @Inject(ILoggerRepository) protected logger: ILoggerRepository, + @Inject(IAccessRepository) protected accessRepository: IAccessRepository, + @Inject(IActivityRepository) protected activityRepository: IActivityRepository, + @Inject(IAuditRepository) protected auditRepository: IAuditRepository, + @Inject(IAlbumRepository) protected albumRepository: IAlbumRepository, + @Inject(IAlbumUserRepository) protected albumUserRepository: IAlbumUserRepository, + @Inject(IAssetRepository) protected assetRepository: IAssetRepository, @Inject(IConfigRepository) protected configRepository: IConfigRepository, + @Inject(ICryptoRepository) protected cryptoRepository: ICryptoRepository, + @Inject(IDatabaseRepository) protected databaseRepository: IDatabaseRepository, + @Inject(IEventRepository) protected eventRepository: IEventRepository, + @Inject(IJobRepository) protected jobRepository: IJobRepository, + @Inject(IKeyRepository) protected keyRepository: IKeyRepository, + @Inject(ILibraryRepository) protected libraryRepository: ILibraryRepository, + @Inject(IMachineLearningRepository) protected machineLearningRepository: IMachineLearningRepository, + @Inject(IMapRepository) protected mapRepository: IMapRepository, + @Inject(IMediaRepository) protected mediaRepository: IMediaRepository, + @Inject(IMemoryRepository) protected memoryRepository: IMemoryRepository, + @Inject(IMetadataRepository) protected metadataRepository: IMetadataRepository, + @Inject(IMetricRepository) protected metricRepository: IMetricRepository, + @Inject(IMoveRepository) protected moveRepository: IMoveRepository, + @Inject(INotificationRepository) protected notificationRepository: INotificationRepository, + @Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository, + @Inject(IPersonRepository) protected personRepository: IPersonRepository, + @Inject(ISearchRepository) protected searchRepository: ISearchRepository, + @Inject(IServerInfoRepository) protected serverInfoRepository: IServerInfoRepository, + @Inject(ISessionRepository) protected sessionRepository: ISessionRepository, + @Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository, + @Inject(IStackRepository) protected stackRepository: IStackRepository, + @Inject(IStorageRepository) protected storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) protected logger: ILoggerRepository, - ) {} + @Inject(ITagRepository) protected tagRepository: ITagRepository, + @Inject(ITrashRepository) protected trashRepository: ITrashRepository, + @Inject(IUserRepository) protected userRepository: IUserRepository, + @Inject(IVersionHistoryRepository) protected versionRepository: IVersionHistoryRepository, + @Inject(IViewRepository) protected viewRepository: IViewRepository, + ) { + this.logger.setContext(this.constructor.name); + this.storageCore = StorageCore.create( + assetRepository, + configRepository, + cryptoRepository, + moveRepository, + personRepository, + storageRepository, + systemMetadataRepository, + this.logger, + ); + } private get repos() { return { diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index f79c2d49342f9..3ccc122eceeb0 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -1,34 +1,16 @@ -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { CliService } from 'src/services/cli.service'; import { userStub } from 'test/fixtures/user.stub'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, describe, it } from 'vitest'; describe(CliService.name, () => { let sut: CliService; - let configMock: Mocked; - let cryptoMock: Mocked; let userMock: Mocked; - let systemMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock); + ({ sut, userMock } = newTestService(CliService)); }); describe('resetAdminPassword', () => { diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 5abd1fab2906b..18a79108c4468 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,26 +1,10 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; @Injectable() export class CliService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(CliService.name); - } - async listUsers(): Promise { const users = await this.userRepository.getList({ withDeleted: true }); return users.map((user) => mapUserAdmin(user)); diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index 5bce6d819ccb8..e28bf1649a3d1 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -7,13 +7,13 @@ import { } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DatabaseService } from 'src/services/database.service'; -import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(DatabaseService.name, () => { let sut: DatabaseService; + let configMock: Mocked; let databaseMock: Mocked; let loggerMock: Mocked; @@ -24,11 +24,7 @@ describe(DatabaseService.name, () => { let versionAboveRange: string; beforeEach(() => { - configMock = newConfigRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new DatabaseService(configMock, databaseMock, loggerMock); + ({ sut, configMock, databaseMock, loggerMock } = newTestService(DatabaseService)); extensionRange = '0.2.x'; databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index 9ba190d30afae..363266c6aef6a 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,17 +1,15 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Duration } from 'luxon'; import semver from 'semver'; import { OnEvent } from 'src/decorators'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseExtension, DatabaseLock, EXTENSION_NAMES, - IDatabaseRepository, VectorExtension, VectorIndex, } from 'src/interfaces/database.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { BaseService } from 'src/services/base.service'; type CreateFailedArgs = { name: string; extension: string; otherName: string }; type UpdateFailedArgs = { name: string; extension: string; availableVersion: string }; @@ -63,17 +61,9 @@ const messages = { const RETRY_DURATION = Duration.fromObject({ seconds: 5 }); @Injectable() -export class DatabaseService { +export class DatabaseService extends BaseService { private reconnection?: NodeJS.Timeout; - constructor( - @Inject(IConfigRepository) private configRepository: IConfigRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - ) { - this.logger.setContext(DatabaseService.name); - } - @OnEvent({ name: 'app.bootstrap', priority: -200 }) async onBootstrap() { const version = await this.databaseRepository.getPostgresVersion(); diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 14fa7bab48f48..2bebd5c320ca2 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -2,15 +2,12 @@ import { BadRequestException } from '@nestjs/common'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Readable } from 'typeorm/platform/PlatformTools.js'; import { Mocked, vitest } from 'vitest'; @@ -28,7 +25,6 @@ describe(DownloadService.name, () => { let sut: DownloadService; let accessMock: IAccessRepositoryMock; let assetMock: Mocked; - let loggerMock: Mocked; let storageMock: Mocked; it('should work', () => { @@ -36,12 +32,7 @@ describe(DownloadService.name, () => { }); beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - storageMock = newStorageRepositoryMock(); - - sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock); + ({ sut, accessMock, assetMock, storageMock } = newTestService(DownloadService)); }); describe('downloadArchive', () => { diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 988b859ff882f..d8ad67044e054 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; @@ -6,26 +6,15 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; +import { ImmichReadStream } from 'src/interfaces/storage.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; import { getPreferences } from 'src/utils/preferences'; @Injectable() -export class DownloadService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - ) { - this.logger.setContext(DownloadService.name); - } - +export class DownloadService extends BaseService { async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise { const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const archives: DownloadArchiveInfo[] = []; @@ -73,7 +62,7 @@ export class DownloadService { } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); @@ -116,20 +105,20 @@ export class DownloadService { if (dto.assetIds) { const assetIds = dto.assetIds; - await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); } if (dto.albumId) { const albumId = dto.albumId; - await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); } if (dto.userId) { const userId = dto.userId; - await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), ); diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index ff03f02389db3..dd2656d34bb1b 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -1,6 +1,4 @@ import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; @@ -8,37 +6,22 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { DuplicateService } from 'src/services/duplicate.service'; import { SearchService } from 'src/services/search.service'; import { assetStub } from 'test/fixtures/asset.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, beforeEach, vitest } from 'vitest'; vitest.useFakeTimers(); describe(SearchService.name, () => { let sut: DuplicateService; + let assetMock: Mocked; - let configMock: Mocked; - let systemMock: Mocked; - let searchMock: Mocked; - let loggerMock: Mocked; - let cryptoMock: Mocked; let jobMock: Mocked; + let loggerMock: Mocked; + let searchMock: Mocked; + let systemMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - searchMock = newSearchRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - jobMock = newJobRepositoryMock(); - - sut = new DuplicateService(configMock, systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock); + ({ sut, assetMock, jobMock, loggerMock, searchMock, systemMock } = newTestService(DuplicateService)); }); it('should work', () => { diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index f5baa611ff044..e76b80b04391c 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -1,22 +1,11 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { - IBaseJob, - IEntityJob, - IJobRepository, - JOBS_ASSET_PAGINATION_SIZE, - JobName, - JobStatus, -} from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { WithoutProperty } from 'src/interfaces/asset.interface'; +import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { AssetDuplicateResult } from 'src/interfaces/search.interface'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; @@ -24,19 +13,6 @@ import { usePagination } from 'src/utils/pagination'; @Injectable() export class DuplicateService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ISearchRepository) private searchRepository: ISearchRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(DuplicateService.name); - } - async getDuplicates(auth: AuthDto): Promise { const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] }); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 8b23e133d97a2..c70e5ab4f9b69 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,8 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { defaults } from 'src/config'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobCommand, @@ -12,20 +10,10 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMetricRepository } from 'src/interfaces/metric.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { JobService } from 'src/services/job.service'; import { assetStub } from 'test/fixtures/asset.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, vitest } from 'vitest'; const makeMockHandlers = (status: JobStatus) => { @@ -39,24 +27,11 @@ const makeMockHandlers = (status: JobStatus) => { describe(JobService.name, () => { let sut: JobService; let assetMock: Mocked; - let configMock: Mocked; - let eventMock: Mocked; let jobMock: Mocked; - let personMock: Mocked; - let metricMock: Mocked; let systemMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - personMock = newPersonRepositoryMock(); - metricMock = newMetricRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - sut = new JobService(assetMock, configMock, eventMock, jobMock, systemMock, personMock, metricMock, loggerMock); + ({ sut, assetMock, jobMock, systemMock } = newTestService(JobService)); }); it('should work', () => { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 7ff76447968bc..e18bad28d6ab0 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -1,15 +1,12 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { snakeCase } from 'lodash'; import { OnEvent } from 'src/decorators'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AssetType, ManualJobName } from 'src/enum'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { ConcurrentQueueName, - IJobRepository, JobCommand, JobHandler, JobItem, @@ -18,10 +15,6 @@ import { QueueCleanType, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMetricRepository } from 'src/interfaces/metric.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BaseService } from 'src/services/base.service'; const asJobItem = (dto: JobCreateDto): JobItem => { @@ -48,20 +41,6 @@ const asJobItem = (dto: JobCreateDto): JobItem => { export class JobService extends BaseService { private isMicroservices = false; - constructor( - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(IMetricRepository) private metricRepository: IMetricRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(JobService.name); - } - @OnEvent({ name: 'app.bootstrap' }) onBootstrap(app: ArgOf<'app.bootstrap'>) { this.isMicroservices = app === 'microservices'; diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 3a6c8446e2f1c..73c3684a9d727 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -5,8 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto'; import { UserEntity } from 'src/entities/user.entity'; import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, @@ -17,7 +15,6 @@ import { JobStatus, } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { LibraryService } from 'src/services/library.service'; @@ -26,15 +23,8 @@ import { authStub } from 'test/fixtures/auth.stub'; import { libraryStub } from 'test/fixtures/library.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { makeMockWatcher } from 'test/repositories/storage.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, vitest } from 'vitest'; async function* mockWalk() { @@ -45,37 +35,14 @@ describe(LibraryService.name, () => { let sut: LibraryService; let assetMock: Mocked; - let configMock: Mocked; - let cryptoMock: Mocked; let databaseMock: Mocked; let jobMock: Mocked; let libraryMock: Mocked; let storageMock: Mocked; let systemMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - libraryMock = newLibraryRepositoryMock(); - assetMock = newAssetRepositoryMock(); - jobMock = newJobRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - storageMock = newStorageRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new LibraryService( - assetMock, - configMock, - cryptoMock, - databaseMock, - jobMock, - libraryMock, - storageMock, - systemMock, - loggerMock, - ); + ({ sut, assetMock, databaseMock, jobMock, libraryMock, storageMock, systemMock } = newTestService(LibraryService)); databaseMock.tryLock.mockResolvedValue(true); }); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index abffad8166133..23cfb07f0a09a 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { R_OK } from 'node:constants'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; @@ -17,24 +17,16 @@ import { import { AssetEntity } from 'src/entities/asset.entity'; import { LibraryEntity } from 'src/entities/library.entity'; import { AssetType } from 'src/enum'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { IEntityJob, - IJobRepository, ILibraryAssetJob, ILibraryFileJob, JobName, JOBS_LIBRARY_PAGINATION_SIZE, JobStatus, } from 'src/interfaces/job.interface'; -import { ILibraryRepository } from 'src/interfaces/library.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BaseService } from 'src/services/base.service'; import { mimeTypes } from 'src/utils/mime-types'; import { handlePromiseError } from 'src/utils/misc'; @@ -47,21 +39,6 @@ export class LibraryService extends BaseService { private watchLock = false; private watchers: Record Promise> = {}; - constructor( - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ILibraryRepository) private repository: ILibraryRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(LibraryService.name); - } - @OnEvent({ name: 'app.bootstrap' }) async onBootstrap() { const config = await this.getConfig({ withCache: false }); @@ -217,14 +194,14 @@ export class LibraryService extends BaseService { return false; } - const libraries = await this.repository.getAll(false); + const libraries = await this.libraryRepository.getAll(false); for (const library of libraries) { await this.watch(library.id); } } async getStatistics(id: string): Promise { - const statistics = await this.repository.getStatistics(id); + const statistics = await this.libraryRepository.getStatistics(id); if (!statistics) { throw new BadRequestException(`Library ${id} not found`); } @@ -237,13 +214,13 @@ export class LibraryService extends BaseService { } async getAll(): Promise { - const libraries = await this.repository.getAll(false); + const libraries = await this.libraryRepository.getAll(false); return libraries.map((library) => mapLibrary(library)); } async handleQueueCleanup(): Promise { this.logger.debug('Cleaning up any pending library deletions'); - const pendingDeletion = await this.repository.getAllDeleted(); + const pendingDeletion = await this.libraryRepository.getAllDeleted(); await this.jobRepository.queueAll( pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), ); @@ -251,7 +228,7 @@ export class LibraryService extends BaseService { } async create(dto: CreateLibraryDto): Promise { - const library = await this.repository.create({ + const library = await this.libraryRepository.create({ ownerId: dto.ownerId, name: dto.name ?? 'New External Library', importPaths: dto.importPaths ?? [], @@ -326,7 +303,7 @@ export class LibraryService extends BaseService { async update(id: string, dto: UpdateLibraryDto): Promise { await this.findOrFail(id); - const library = await this.repository.update({ id, ...dto }); + const library = await this.libraryRepository.update({ id, ...dto }); if (dto.importPaths) { const validation = await this.validate(id, { importPaths: dto.importPaths }); @@ -349,7 +326,7 @@ export class LibraryService extends BaseService { await this.unwatch(id); } - await this.repository.softDelete(id); + await this.libraryRepository.softDelete(id); await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } }); } @@ -379,7 +356,7 @@ export class LibraryService extends BaseService { if (!assetsFound) { this.logger.log(`Deleting library ${libraryId}`); - await this.repository.delete(libraryId); + await this.libraryRepository.delete(libraryId); } return JobStatus.SUCCESS; } @@ -407,7 +384,7 @@ export class LibraryService extends BaseService { this.logger.log(`Importing new library asset: ${assetPath}`); - const library = await this.repository.get(job.id, true); + const library = await this.libraryRepository.get(job.id, true); if (!library || library.deletedAt) { this.logger.error('Cannot import asset into deleted library'); return JobStatus.FAILED; @@ -477,7 +454,7 @@ export class LibraryService extends BaseService { await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); - const libraries = await this.repository.getAll(true); + const libraries = await this.libraryRepository.getAll(true); await this.jobRepository.queueAll( libraries.map((library) => ({ name: JobName.LIBRARY_QUEUE_SYNC_FILES, @@ -553,7 +530,7 @@ export class LibraryService extends BaseService { } async handleQueueSyncFiles(job: IEntityJob): Promise { - const library = await this.repository.get(job.id); + const library = await this.libraryRepository.get(job.id); if (!library) { this.logger.debug(`Library ${job.id} not found, skipping refresh`); return JobStatus.SKIPPED; @@ -598,13 +575,13 @@ export class LibraryService extends BaseService { this.logger.warn(`No valid import paths found for library ${library.id}`); } - await this.repository.update({ id: job.id, refreshedAt: new Date() }); + await this.libraryRepository.update({ id: job.id, refreshedAt: new Date() }); return JobStatus.SUCCESS; } async handleQueueSyncAssets(job: IEntityJob): Promise { - const library = await this.repository.get(job.id); + const library = await this.libraryRepository.get(job.id); if (!library) { return JobStatus.SKIPPED; } @@ -636,7 +613,7 @@ export class LibraryService extends BaseService { } private async findOrFail(id: string) { - const library = await this.repository.get(id); + const library = await this.libraryRepository.get(id); if (!library) { throw new BadRequestException('Library not found'); } diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index e0127b73efbb9..ec3fb00dc84fd 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,26 +1,19 @@ -import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { MapService } from 'src/services/map.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newMapRepositoryMock } from 'test/repositories/map.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(MapService.name, () => { let sut: MapService; - let albumMock: Mocked; - let partnerMock: Mocked; + let mapMock: Mocked; + let partnerMock: Mocked; beforeEach(() => { - albumMock = newAlbumRepositoryMock(); - partnerMock = newPartnerRepositoryMock(); - mapMock = newMapRepositoryMock(); - - sut = new MapService(albumMock, partnerMock, mapMock); + ({ sut, mapMock, partnerMock } = newTestService(MapService)); }); describe('getMapMarkers', () => { diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index 3b1ee58cf124d..860a782e79a0b 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -1,18 +1,9 @@ -import { Inject } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IMapRepository } from 'src/interfaces/map.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { BaseService } from 'src/services/base.service'; import { getMyPartnerIds } from 'src/utils/asset.util'; -export class MapService { - constructor( - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(IMapRepository) private mapRepository: IMapRepository, - ) {} - +export class MapService extends BaseService { async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise { const userIds = [auth.user.id]; if (options.withPartners) { diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index fd0de06926906..54f69bc41083f 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -12,12 +12,9 @@ import { VideoCodec, } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -26,55 +23,23 @@ import { assetStub } from 'test/fixtures/asset.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { probeStub } from 'test/fixtures/media.stub'; import { personStub } from 'test/fixtures/person.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; -import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(MediaService.name, () => { let sut: MediaService; + let assetMock: Mocked; - let configMock: Mocked; let jobMock: Mocked; + let loggerMock: Mocked; let mediaMock: Mocked; - let moveMock: Mocked; let personMock: Mocked; let storageMock: Mocked; let systemMock: Mocked; - let cryptoMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - jobMock = newJobRepositoryMock(); - mediaMock = newMediaRepositoryMock(); - moveMock = newMoveRepositoryMock(); - personMock = newPersonRepositoryMock(); - storageMock = newStorageRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new MediaService( - assetMock, - configMock, - personMock, - jobMock, - mediaMock, - storageMock, - systemMock, - moveMock, - cryptoMock, - loggerMock, - ); + ({ sut, assetMock, jobMock, loggerMock, mediaMock, personMock, storageMock, systemMock } = + newTestService(MediaService)); }); it('should be defined', () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index adb8c54f4a797..e4fd91f363b57 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { dirname } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; @@ -17,31 +17,17 @@ import { VideoCodec, VideoContainer, } from 'src/enum'; -import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface'; import { IBaseJob, IEntityJob, - IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobItem, JobName, JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { - AudioStreamInfo, - IMediaRepository, - TranscodeCommand, - VideoFormat, - VideoStreamInfo, -} from 'src/interfaces/media.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { AudioStreamInfo, TranscodeCommand, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; @@ -50,36 +36,9 @@ import { usePagination } from 'src/utils/pagination'; @Injectable() export class MediaService extends BaseService { - private storageCore: StorageCore; private maliOpenCL?: boolean; private devices?: string[]; - constructor( - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IMediaRepository) private mediaRepository: IMediaRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(MediaService.name); - this.storageCore = StorageCore.create( - assetRepository, - configRepository, - cryptoRepository, - moveRepository, - personRepository, - storageRepository, - systemMetadataRepository, - this.logger, - ); - } - async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index ba184daa801bf..b5dd4c2553f4a 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -5,20 +5,18 @@ import { MemoryService } from 'src/services/memory.service'; import { authStub } from 'test/fixtures/auth.stub'; import { memoryStub } from 'test/fixtures/memory.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(MemoryService.name, () => { + let sut: MemoryService; + let accessMock: IAccessRepositoryMock; let memoryMock: Mocked; - let sut: MemoryService; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - memoryMock = newMemoryRepositoryMock(); - - sut = new MemoryService(accessMock, memoryMock); + ({ sut, accessMock, memoryMock } = newTestService(MemoryService)); }); it('should be defined', () => { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index fb1ff49f0b456..f7d1ead6aaad9 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,28 +1,22 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() -export class MemoryService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IMemoryRepository) private repository: IMemoryRepository, - ) {} - +export class MemoryService extends BaseService { async search(auth: AuthDto) { - const memories = await this.repository.search(auth.user.id); + const memories = await this.memoryRepository.search(auth.user.id); return memories.map((memory) => mapMemory(memory)); } async get(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory); } @@ -31,12 +25,12 @@ export class MemoryService { // TODO validate type/data combination const assetIds = dto.assetIds || []; - const allowedAssetIds = await checkAccess(this.access, { + const allowedAssetIds = await checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: assetIds, }); - const memory = await this.repository.create({ + const memory = await this.memoryRepository.create({ ownerId: auth.user.id, type: dto.type, data: dto.data, @@ -50,9 +44,9 @@ export class MemoryService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const memory = await this.repository.update({ + const memory = await this.memoryRepository.update({ id, isSaved: dto.isSaved, memoryAt: dto.memoryAt, @@ -63,28 +57,28 @@ export class MemoryService { } async remove(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); - await this.repository.delete(id); + await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); + await this.memoryRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_READ, ids: [id] }); - const repos = { access: this.access, bulk: this.repository }; + const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.repository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update({ id, updatedAt: new Date() }); } return results; } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const repos = { access: this.access, bulk: this.repository }; + const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, @@ -93,14 +87,14 @@ export class MemoryService { const hasSuccess = results.find(({ success }) => success); if (hasSuccess) { - await this.repository.update({ id, updatedAt: new Date() }); + await this.memoryRepository.update({ id, updatedAt: new Date() }); } return results; } private async findOrFail(id: string) { - const memory = await this.repository.get(id); + const memory = await this.memoryRepository.get(id); if (!memory) { throw new BadRequestException('Memory not found'); } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 88b2498e91b87..bd1444a002a0b 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -6,16 +6,12 @@ import { ExifEntity } from 'src/entities/exif.entity'; import { AssetType, SourceType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMapRepository } from 'src/interfaces/map.interface'; import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -28,23 +24,7 @@ import { probeStub } from 'test/fixtures/media.stub'; import { metadataStub } from 'test/fixtures/metadata.stub'; import { personStub } from 'test/fixtures/person.stub'; import { tagStub } from 'test/fixtures/tag.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMapRepositoryMock } from 'test/repositories/map.repository.mock'; -import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; -import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; -import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(MetadataService.name, () => { @@ -52,60 +32,35 @@ describe(MetadataService.name, () => { let albumMock: Mocked; let assetMock: Mocked; - let configMock: Mocked; - let cryptoRepository: Mocked; - let databaseMock: Mocked; + let cryptoMock: Mocked; let eventMock: Mocked; let jobMock: Mocked; let mapMock: Mocked; - let metadataMock: Mocked; - let moveMock: Mocked; let mediaMock: Mocked; + let metadataMock: Mocked; let personMock: Mocked; let storageMock: Mocked; let systemMock: Mocked; let tagMock: Mocked; let userMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - albumMock = newAlbumRepositoryMock(); - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - cryptoRepository = newCryptoRepositoryMock(); - jobMock = newJobRepositoryMock(); - mapMock = newMapRepositoryMock(); - metadataMock = newMetadataRepositoryMock(); - moveMock = newMoveRepositoryMock(); - personMock = newPersonRepositoryMock(); - eventMock = newEventRepositoryMock(); - storageMock = newStorageRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - mediaMock = newMediaRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - tagMock = newTagRepositoryMock(); - - sut = new MetadataService( + ({ + sut, albumMock, assetMock, - configMock, - cryptoRepository, - databaseMock, + cryptoMock, eventMock, jobMock, mapMock, mediaMock, metadataMock, - moveMock, personMock, storageMock, systemMock, tagMock, userMock, - loggerMock, - ); + } = newTestService(MetadataService)); }); afterEach(async () => { @@ -569,10 +524,10 @@ describe(MetadataService.name, () => { EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoType: 'MotionPhoto_Data', }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); const video = randomBytes(512); metadataMock.extractBinaryTag.mockResolvedValue(video); @@ -612,10 +567,10 @@ describe(MetadataService.name, () => { EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoType: 'MotionPhoto_Data', }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); const video = randomBytes(512); metadataMock.extractBinaryTag.mockResolvedValue(video); @@ -656,10 +611,10 @@ describe(MetadataService.name, () => { MicroVideo: 1, MicroVideoOffset: 1, }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); @@ -700,7 +655,7 @@ describe(MetadataService.name, () => { MicroVideo: 1, MicroVideoOffset: 1, }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset })); const video = randomBytes(512); @@ -725,7 +680,7 @@ describe(MetadataService.name, () => { MicroVideo: 1, MicroVideoOffset: 1, }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset); const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); @@ -747,7 +702,7 @@ describe(MetadataService.name, () => { MicroVideo: 1, MicroVideoOffset: 1, }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true }); const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); @@ -773,7 +728,7 @@ describe(MetadataService.name, () => { MicroVideo: 1, MicroVideoOffset: 1, }); - cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + cryptoMock.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); const video = randomBytes(512); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 3995c72f770e5..e0f93abf406f6 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import _ from 'lodash'; @@ -13,32 +13,20 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetType, SourceType } from 'src/enum'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; +import { WithoutProperty } from 'src/interfaces/asset.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, - IJobRepository, ISidecarWriteJob, JobName, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; -import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { ITagRepository } from 'src/interfaces/tag.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; +import { ReverseGeocodeResult } from 'src/interfaces/map.interface'; +import { ImmichTags } from 'src/interfaces/metadata.interface'; import { BaseService } from 'src/services/base.service'; import { isFaceImportEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -99,41 +87,6 @@ const validateRange = (value: number | undefined, min: number, max: number): Non @Injectable() export class MetadataService extends BaseService { - private storageCore: StorageCore; - - constructor( - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IMapRepository) private mapRepository: IMapRepository, - @Inject(IMediaRepository) private mediaRepository: IMediaRepository, - @Inject(IMetadataRepository) private repository: IMetadataRepository, - @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ITagRepository) private tagRepository: ITagRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(MetadataService.name); - this.storageCore = StorageCore.create( - assetRepository, - configRepository, - cryptoRepository, - moveRepository, - personRepository, - storageRepository, - systemMetadataRepository, - this.logger, - ); - } - @OnEvent({ name: 'app.bootstrap' }) async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { @@ -145,7 +98,7 @@ export class MetadataService extends BaseService { @OnEvent({ name: 'app.shutdown' }) async onShutdown() { - await this.repository.teardown(); + await this.metadataRepository.teardown(); } @OnEvent({ name: 'config.update' }) @@ -372,7 +325,7 @@ export class MetadataService extends BaseService { return JobStatus.SKIPPED; } - await this.repository.writeTags(sidecarPath, exif); + await this.metadataRepository.writeTags(sidecarPath, exif); if (!asset.sidecarPath) { await this.assetRepository.update({ id, sidecarPath }); @@ -382,8 +335,8 @@ export class MetadataService extends BaseService { } private async getExifTags(asset: AssetEntity): Promise { - const mediaTags = await this.repository.readTags(asset.originalPath); - const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {}; + const mediaTags = await this.metadataRepository.readTags(asset.originalPath); + const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {}; const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {}; // make sure dates comes from sidecar @@ -467,11 +420,11 @@ export class MetadataService extends BaseService { // Samsung MotionPhoto video extraction // HEIC-encoded if (hasMotionPhotoVideo) { - video = await this.repository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo'); + video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo'); } // JPEG-encoded; HEIC also contains these tags, so this conditional must come second else if (hasEmbeddedVideoFile) { - video = await this.repository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile'); + video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile'); } // Default video extraction else { diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 6b3c9e6895f8d..548980bdd909f 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -6,10 +6,8 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; @@ -17,15 +15,7 @@ import { NotificationService } from 'src/services/notification.service'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; const configs = { @@ -66,39 +56,19 @@ const configs = { }; describe(NotificationService.name, () => { + let sut: NotificationService; + let albumMock: Mocked; let assetMock: Mocked; - let configMock: Mocked; let eventMock: Mocked; let jobMock: Mocked; - let loggerMock: Mocked; let notificationMock: Mocked; - let sut: NotificationService; let systemMock: Mocked; let userMock: Mocked; beforeEach(() => { - albumMock = newAlbumRepositoryMock(); - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - notificationMock = newNotificationRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - userMock = newUserRepositoryMock(); - - sut = new NotificationService( - configMock, - eventMock, - systemMock, - notificationMock, - userMock, - jobMock, - loggerMock, - assetMock, - albumMock, - ); + ({ sut, albumMock, assetMock, eventMock, jobMock, notificationMock, systemMock, userMock } = + newTestService(NotificationService)); }); it('should work', () => { diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index dce13e5f6c60e..f6b338d79e716 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,25 +1,18 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IEmailJob, - IJobRepository, INotifyAlbumInviteJob, INotifyAlbumUpdateJob, INotifySignupJob, JobName, JobStatus, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; +import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; @@ -28,21 +21,6 @@ import { getPreferences } from 'src/utils/preferences'; @Injectable() export class NotificationService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(INotificationRepository) private notificationRepository: INotificationRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(NotificationService.name); - } - @OnEvent({ name: 'config.update' }) onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { this.eventRepository.clientBroadcast('on_config_update'); diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index b2b3401251cb6..a1433eed4d82b 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -1,20 +1,17 @@ import { BadRequestException } from '@nestjs/common'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface'; import { PartnerService } from 'src/services/partner.service'; import { authStub } from 'test/fixtures/auth.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(PartnerService.name, () => { let sut: PartnerService; let partnerMock: Mocked; - let accessMock: Mocked; beforeEach(() => { - partnerMock = newPartnerRepositoryMock(); - sut = new PartnerService(partnerMock, accessMock); + ({ sut, partnerMock } = newTestService(PartnerService)); }); it('should work', () => { diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index 4b7cd4c516e42..39907ec5fe11f 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,43 +1,38 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; import { PartnerEntity } from 'src/entities/partner.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; +import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; @Injectable() -export class PartnerService { - constructor( - @Inject(IPartnerRepository) private repository: IPartnerRepository, - @Inject(IAccessRepository) private access: IAccessRepository, - ) {} - +export class PartnerService extends BaseService { async create(auth: AuthDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; - const exists = await this.repository.get(partnerId); + const exists = await this.partnerRepository.get(partnerId); if (exists) { throw new BadRequestException(`Partner already exists`); } - const partner = await this.repository.create(partnerId); + const partner = await this.partnerRepository.create(partnerId); return this.mapPartner(partner, PartnerDirection.SharedBy); } async remove(auth: AuthDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; - const partner = await this.repository.get(partnerId); + const partner = await this.partnerRepository.get(partnerId); if (!partner) { throw new BadRequestException('Partner not found'); } - await this.repository.remove(partner); + await this.partnerRepository.remove(partner); } async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise { - const partners = await this.repository.getAll(auth.user.id); + const partners = await this.partnerRepository.getAll(auth.user.id); const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; return partners .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users @@ -46,10 +41,10 @@ export class PartnerService { } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; - const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); + const entity = await this.partnerRepository.update({ ...partnerId, inTimeline: dto.inTimeline }); return this.mapPartner(entity, PartnerDirection.SharedWith); } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index eb5362d62b57c..2d2884c83fd62 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -4,13 +4,10 @@ import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DetectedFaces, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMediaRepository } from 'src/interfaces/media.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -22,19 +19,8 @@ import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { personStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; -import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; -import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { IsNull } from 'typeorm'; import { Mocked } from 'vitest'; @@ -67,51 +53,33 @@ const detectFaceMock: DetectedFaces = { }; describe(PersonService.name, () => { + let sut: PersonService; + let accessMock: IAccessRepositoryMock; let assetMock: Mocked; - let configMock: Mocked; - let systemMock: Mocked; + let cryptoMock: Mocked; let jobMock: Mocked; let machineLearningMock: Mocked; let mediaMock: Mocked; - let moveMock: Mocked; let personMock: Mocked; - let storageMock: Mocked; let searchMock: Mocked; - let cryptoMock: Mocked; - let loggerMock: Mocked; - let sut: PersonService; + let storageMock: Mocked; + let systemMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - jobMock = newJobRepositoryMock(); - machineLearningMock = newMachineLearningRepositoryMock(); - moveMock = newMoveRepositoryMock(); - mediaMock = newMediaRepositoryMock(); - personMock = newPersonRepositoryMock(); - storageMock = newStorageRepositoryMock(); - searchMock = newSearchRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new PersonService( + ({ + sut, accessMock, assetMock, - configMock, + cryptoMock, + jobMock, machineLearningMock, - moveMock, mediaMock, personMock, - systemMock, - storageMock, - jobMock, searchMock, - cryptoMock, - loggerMock, - ); + storageMock, + systemMock, + } = newTestService(PersonService)); }); it('should be defined', () => { diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 3b71d3504e33b..c364e0b1f5a0a 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; @@ -31,15 +31,11 @@ import { SourceType, SystemMetadataKey, } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { WithoutProperty } from 'src/interfaces/asset.interface'; import { IBaseJob, IDeferrableJob, IEntityJob, - IJobRepository, INightlyJob, JOBS_ASSET_PAGINATION_SIZE, JobItem, @@ -47,14 +43,9 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { BoundingBox, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { CropOptions, IMediaRepository, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; -import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface'; -import { ISearchRepository } from 'src/interfaces/search.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BoundingBox } from 'src/interfaces/machine-learning.interface'; +import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface'; +import { UpdateFacesData } from 'src/interfaces/person.interface'; import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; @@ -66,37 +57,6 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService extends BaseService { - private storageCore: StorageCore; - - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, - @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(IMediaRepository) private mediaRepository: IMediaRepository, - @Inject(IPersonRepository) private repository: IPersonRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ISearchRepository) private smartInfoRepository: ISearchRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(PersonService.name); - this.storageCore = StorageCore.create( - assetRepository, - configRepository, - cryptoRepository, - moveRepository, - repository, - storageRepository, - systemMetadataRepository, - this.logger, - ); - } - async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { const { withHidden = false, page, size } = dto; const pagination = { @@ -105,11 +65,11 @@ export class PersonService extends BaseService { }; const { machineLearning } = await this.getConfig({ withCache: false }); - const { items, hasNextPage } = await this.repository.getAllForUser(pagination, auth.user.id, { + const { items, hasNextPage } = await this.personRepository.getAllForUser(pagination, auth.user.id, { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden, }); - const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id); + const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id); return { people: items.map((person) => mapPerson(person)), @@ -120,15 +80,15 @@ export class PersonService extends BaseService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; for (const data of dto.data) { - const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); + const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -136,7 +96,7 @@ export class PersonService extends BaseService { changeFeaturePhoto.push(face.person.id); } - await this.repository.reassignFace(face.id, personId); + await this.personRepository.reassignFace(face.id, personId); } result.push(person); @@ -149,12 +109,12 @@ export class PersonService extends BaseService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); - await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); - const face = await this.repository.getFaceById(dto.id); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); + const face = await this.personRepository.getFaceById(dto.id); const person = await this.findOrFail(personId); - await this.repository.reassignFace(face.id, personId); + await this.personRepository.reassignFace(face.id, personId); if (person.faceAssetId === null) { await this.createNewFeaturePhoto([person.id]); } @@ -166,8 +126,8 @@ export class PersonService extends BaseService { } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); - const faces = await this.repository.getFaces(dto.id); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); + const faces = await this.personRepository.getFaces(dto.id); return faces.map((asset) => mapFaces(asset, auth)); } @@ -178,10 +138,10 @@ export class PersonService extends BaseService { const jobs: JobItem[] = []; for (const personId of changeFeaturePhoto) { - const assetFace = await this.repository.getRandomFace(personId); + const assetFace = await this.personRepository.getRandomFace(personId); if (assetFace !== null) { - await this.repository.update({ id: personId, faceAssetId: assetFace.id }); + await this.personRepository.update({ id: personId, faceAssetId: assetFace.id }); jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } }); } } @@ -190,18 +150,18 @@ export class PersonService extends BaseService { } async getById(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.findOrFail(id).then(mapPerson); } async getStatistics(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); - return this.repository.getStatistics(id); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] }); + return this.personRepository.getStatistics(id); } async getThumbnail(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); - const person = await this.repository.getById(id); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] }); + const person = await this.personRepository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); } @@ -214,13 +174,13 @@ export class PersonService extends BaseService { } async getAssets(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); - const assets = await this.repository.getAssets(id); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] }); + const assets = await this.personRepository.getAssets(id); return assets.map((asset) => mapAsset(asset)); } create(auth: AuthDto, dto: PersonCreateDto): Promise { - return this.repository.create({ + return this.personRepository.create({ ownerId: auth.user.id, name: dto.name, birthDate: dto.birthDate, @@ -229,14 +189,14 @@ export class PersonService extends BaseService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; // TODO: set by faceId directly let faceId: string | undefined = undefined; if (assetId) { - await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); - const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); + const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); } @@ -244,7 +204,7 @@ export class PersonService extends BaseService { faceId = face.id; } - const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden }); + const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden }); if (assetId) { await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); @@ -274,12 +234,12 @@ export class PersonService extends BaseService { private async delete(people: PersonEntity[]) { await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath))); - await this.repository.delete(people); + await this.personRepository.delete(people); this.logger.debug(`Deleted ${people.length} people`); } async handlePersonCleanup(): Promise { - const people = await this.repository.getAllWithoutFaces(); + const people = await this.personRepository.getAllWithoutFaces(); await this.delete(people); return JobStatus.SUCCESS; } @@ -291,7 +251,7 @@ export class PersonService extends BaseService { } if (force) { - await this.repository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); } @@ -364,7 +324,7 @@ export class PersonService extends BaseService { }); } - const faceIds = await this.repository.createFaces(mappedFaces); + const faceIds = await this.personRepository.createFaces(mappedFaces); await this.jobRepository.queueAll(faceIds.map((id) => ({ name: JobName.FACIAL_RECOGNITION, data: { id } }))); } @@ -387,7 +347,7 @@ export class PersonService extends BaseService { if (nightly) { const [state, latestFaceDate] = await Promise.all([ this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE), - this.repository.getLatestFaceDate(), + this.personRepository.getLatestFaceDate(), ]); if (state?.lastRun && latestFaceDate && state.lastRun > latestFaceDate) { @@ -399,7 +359,7 @@ export class PersonService extends BaseService { const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION); if (force) { - await this.repository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); } else if (waiting) { this.logger.debug( @@ -410,7 +370,7 @@ export class PersonService extends BaseService { const lastRun = new Date().toISOString(); const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.repository.getAllFaces(pagination, { + this.personRepository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull(), sourceType: IsNull() }, }), ); @@ -432,7 +392,7 @@ export class PersonService extends BaseService { return JobStatus.SKIPPED; } - const face = await this.repository.getFaceByIdWithAssets( + const face = await this.personRepository.getFaceByIdWithAssets( id, { person: true, asset: true, faceSearch: true }, { id: true, personId: true, sourceType: true, faceSearch: { embedding: true } }, @@ -457,7 +417,7 @@ export class PersonService extends BaseService { return JobStatus.SKIPPED; } - const matches = await this.smartInfoRepository.searchFaces({ + const matches = await this.searchRepository.searchFaces({ userIds: [face.asset.ownerId], embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, @@ -481,7 +441,7 @@ export class PersonService extends BaseService { let personId = matches.find((match) => match.face.personId)?.face.personId; if (!personId) { - const matchWithPerson = await this.smartInfoRepository.searchFaces({ + const matchWithPerson = await this.searchRepository.searchFaces({ userIds: [face.asset.ownerId], embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, @@ -496,21 +456,21 @@ export class PersonService extends BaseService { if (isCore && !personId) { this.logger.log(`Creating new person for face ${id}`); - const newPerson = await this.repository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id }); + const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id }); await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } }); personId = newPerson.id; } if (personId) { this.logger.debug(`Assigning face ${id} to person ${personId}`); - await this.repository.reassignFaces({ faceIds: [id], newPersonId: personId }); + await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId }); } return JobStatus.SUCCESS; } async handlePersonMigration({ id }: IEntityJob): Promise { - const person = await this.repository.getById(id); + const person = await this.personRepository.getById(id); if (!person) { return JobStatus.FAILED; } @@ -526,13 +486,13 @@ export class PersonService extends BaseService { return JobStatus.SKIPPED; } - const person = await this.repository.getById(data.id); + const person = await this.personRepository.getById(data.id); if (!person?.faceAssetId) { this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`); return JobStatus.FAILED; } - const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId); + const face = await this.personRepository.getFaceByIdWithAssets(person.faceAssetId); if (face === null) { this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`); return JobStatus.FAILED; @@ -572,7 +532,7 @@ export class PersonService extends BaseService { }; await this.mediaRepository.generateThumbnail(inputPath, thumbnailOptions, thumbnailPath); - await this.repository.update({ id: person.id, thumbnailPath }); + await this.personRepository.update({ id: person.id, thumbnailPath }); return JobStatus.SUCCESS; } @@ -583,13 +543,13 @@ export class PersonService extends BaseService { throw new BadRequestException('Cannot merge a person into themselves'); } - await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; const results: BulkIdResponseDto[] = []; - const allowedIds = await checkAccess(this.access, { + const allowedIds = await checkAccess(this.accessRepository, { auth, permission: Permission.PERSON_MERGE, ids: mergeIds, @@ -603,7 +563,7 @@ export class PersonService extends BaseService { } try { - const mergePerson = await this.repository.getById(mergeId); + const mergePerson = await this.personRepository.getById(mergeId); if (!mergePerson) { results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND }); continue; @@ -619,14 +579,14 @@ export class PersonService extends BaseService { } if (Object.keys(update).length > 0) { - primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update }); + primaryPerson = await this.personRepository.update({ id: primaryPerson.id, ...update }); } const mergeName = mergePerson.name || mergePerson.id; const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id }; this.logger.log(`Merging ${mergeName} into ${primaryName}`); - await this.repository.reassignFaces(mergeData); + await this.personRepository.reassignFaces(mergeData); await this.delete([mergePerson]); this.logger.log(`Merged ${mergeName} into ${primaryName}`); @@ -640,7 +600,7 @@ export class PersonService extends BaseService { } private async findOrFail(id: string) { - const person = await this.repository.getById(id); + const person = await this.personRepository.getById(id); if (!person) { throw new BadRequestException('Person not found'); } diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index eb30717a3ad8d..e0b03f31aee3b 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -1,60 +1,26 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SearchService } from 'src/services/search.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { personStub } from 'test/fixtures/person.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, beforeEach, vitest } from 'vitest'; vitest.useFakeTimers(); describe(SearchService.name, () => { let sut: SearchService; + let assetMock: Mocked; - let configMock: Mocked; - let systemMock: Mocked; - let machineMock: Mocked; let personMock: Mocked; let searchMock: Mocked; - let partnerMock: Mocked; - let loggerMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - machineMock = newMachineLearningRepositoryMock(); - personMock = newPersonRepositoryMock(); - searchMock = newSearchRepositoryMock(); - partnerMock = newPartnerRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new SearchService( - configMock, - systemMock, - machineMock, - personMock, - searchMock, - assetMock, - partnerMock, - loggerMock, - ); + ({ sut, assetMock, personMock, searchMock } = newTestService(SearchService)); }); it('should work', () => { diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index b878b4e89808e..03ffbe97db14e 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { PersonResponseDto } from 'src/dtos/person.dto'; @@ -16,34 +16,13 @@ import { } from 'src/dtos/search.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetOrder } from 'src/enum'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { SearchExploreItem } from 'src/interfaces/search.interface'; import { BaseService } from 'src/services/base.service'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() export class SearchService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, - @Inject(IPersonRepository) private personRepository: IPersonRepository, - @Inject(ISearchRepository) private searchRepository: ISearchRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(SearchService.name); - } - async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise { return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); } @@ -108,7 +87,11 @@ export class SearchService extends BaseService { const userIds = await this.getUserIdsToSearch(auth); - const embedding = await this.machineLearning.encodeText(machineLearning.url, dto.query, machineLearning.clip); + const embedding = await this.machineLearningRepository.encodeText( + machineLearning.url, + dto.query, + machineLearning.clip, + ); const page = dto.page ?? 1; const size = dto.size || 100; const { hasNextPage, items } = await this.searchRepository.searchSmart( diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 18e7bde1dc479..ab6eb3b1a4f05 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -1,41 +1,20 @@ import { SystemMetadataKey } from 'src/enum'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { ServerService } from 'src/services/server.service'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(ServerService.name, () => { let sut: ServerService; - let configMock: Mocked; + let storageMock: Mocked; - let userMock: Mocked; - let serverInfoMock: Mocked; let systemMock: Mocked; - let loggerMock: Mocked; - let cryptoMock: Mocked; + let userMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - storageMock = newStorageRepositoryMock(); - userMock = newUserRepositoryMock(); - serverInfoMock = newServerInfoRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - - sut = new ServerService(configMock, userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock); + ({ sut, storageMock, systemMock, userMock } = newTestService(ServerService)); }); it('should work', () => { diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index ffab0c5a893dc..22c071f73e3d8 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; @@ -15,13 +15,7 @@ import { UsageByUserDto, } from 'src/dtos/server.dto'; import { StorageFolder, SystemMetadataKey } from 'src/enum'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface'; +import { UserStatsQueryResponse } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; import { asHumanReadable } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; @@ -29,19 +23,6 @@ import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchE @Injectable() export class ServerService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(ServerService.name); - } - @OnEvent({ name: 'app.bootstrap' }) async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index ca3d2fd858fb0..49d122771210d 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -1,27 +1,21 @@ import { UserEntity } from 'src/entities/user.entity'; import { JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; import { SessionService } from 'src/services/session.service'; import { authStub } from 'test/fixtures/auth.stub'; import { sessionStub } from 'test/fixtures/session.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe('SessionService', () => { let sut: SessionService; + let accessMock: Mocked; - let loggerMock: Mocked; let sessionMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - sessionMock = newSessionRepositoryMock(); - - sut = new SessionService(accessMock, loggerMock, sessionMock); + ({ sut, accessMock, sessionMock } = newTestService(SessionService)); }); it('should be defined', () => { diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 47abf3c380246..c68fb3088c9aa 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,24 +1,14 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISessionRepository } from 'src/interfaces/session.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; @Injectable() -export class SessionService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - @Inject(ISessionRepository) private sessionRepository: ISessionRepository, - ) { - this.logger.setContext(SessionService.name); - } - +export class SessionService extends BaseService { async handleCleanup() { const sessions = await this.sessionRepository.search({ updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(), @@ -44,7 +34,7 @@ export class SessionService { } async delete(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); await this.sessionRepository.delete(id); } diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 28afe94b9f2e2..4e54108f6d04b 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -3,42 +3,24 @@ import _ from 'lodash'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { SharedLinkType } from 'src/enum'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SharedLinkService } from 'src/services/shared-link.service'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(SharedLinkService.name, () => { let sut: SharedLinkService; + let accessMock: IAccessRepositoryMock; - let configMock: Mocked; - let cryptoMock: Mocked; - let shareMock: Mocked; - let systemMock: Mocked; - let logMock: Mocked; + let sharedLinkMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - configMock = newConfigRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - shareMock = newSharedLinkRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - logMock = newLoggerRepositoryMock(); - - sut = new SharedLinkService(accessMock, configMock, cryptoMock, logMock, shareMock, systemMock); + ({ sut, accessMock, sharedLinkMock } = newTestService(SharedLinkService)); }); it('should work', () => { @@ -47,55 +29,55 @@ describe(SharedLinkService.name, () => { describe('getAll', () => { it('should return all shared links for a user', async () => { - shareMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]); + sharedLinkMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]); await expect(sut.getAll(authStub.user1)).resolves.toEqual([ sharedLinkResponseStub.expired, sharedLinkResponseStub.valid, ]); - expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); + expect(sharedLinkMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); }); }); describe('getMine', () => { it('should only work for a public user', async () => { await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException); - expect(shareMock.get).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).not.toHaveBeenCalled(); }); it('should return the shared link for the public user', async () => { const authDto = authStub.adminSharedLink; - shareMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid); - expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); it('should not return metadata', async () => { const authDto = authStub.adminSharedLinkNoExif; - shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); - expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); it('should throw an error for an password protected shared link', async () => { const authDto = authStub.adminSharedLink; - shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); - expect(shareMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); }); describe('get', () => { it('should throw an error for an invalid shared link', async () => { - shareMock.get.mockResolvedValue(null); + sharedLinkMock.get.mockResolvedValue(null); await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); - expect(shareMock.update).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); + expect(sharedLinkMock.update).not.toHaveBeenCalled(); }); it('should get a shared link by id', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.get(authStub.user1, sharedLinkStub.valid.id)).resolves.toEqual(sharedLinkResponseStub.valid); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); }); }); @@ -126,7 +108,7 @@ describe(SharedLinkService.name, () => { it('should create an album shared link', async () => { accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); - shareMock.create.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.create.mockResolvedValue(sharedLinkStub.valid); await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); @@ -134,7 +116,7 @@ describe(SharedLinkService.name, () => { authStub.admin.user.id, new Set([albumStub.oneAsset.id]), ); - expect(shareMock.create).toHaveBeenCalledWith({ + expect(sharedLinkMock.create).toHaveBeenCalledWith({ type: SharedLinkType.ALBUM, userId: authStub.admin.user.id, albumId: albumStub.oneAsset.id, @@ -150,7 +132,7 @@ describe(SharedLinkService.name, () => { it('should create an individual shared link', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - shareMock.create.mockResolvedValue(sharedLinkStub.individual); + sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, @@ -164,7 +146,7 @@ describe(SharedLinkService.name, () => { authStub.admin.user.id, new Set([assetStub.image.id]), ); - expect(shareMock.create).toHaveBeenCalledWith({ + expect(sharedLinkMock.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, userId: authStub.admin.user.id, albumId: null, @@ -180,7 +162,7 @@ describe(SharedLinkService.name, () => { it('should create a shared link with allowDownload set to false when showMetadata is false', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - shareMock.create.mockResolvedValue(sharedLinkStub.individual); + sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, @@ -194,7 +176,7 @@ describe(SharedLinkService.name, () => { authStub.admin.user.id, new Set([assetStub.image.id]), ); - expect(shareMock.create).toHaveBeenCalledWith({ + expect(sharedLinkMock.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, userId: authStub.admin.user.id, albumId: null, @@ -211,18 +193,18 @@ describe(SharedLinkService.name, () => { describe('update', () => { it('should throw an error for an invalid shared link', async () => { - shareMock.get.mockResolvedValue(null); + sharedLinkMock.get.mockResolvedValue(null); await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); - expect(shareMock.update).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); + expect(sharedLinkMock.update).not.toHaveBeenCalled(); }); it('should update a shared link', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.valid); - shareMock.update.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.update.mockResolvedValue(sharedLinkStub.valid); await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false }); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); - expect(shareMock.update).toHaveBeenCalledWith({ + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); + expect(sharedLinkMock.update).toHaveBeenCalledWith({ id: sharedLinkStub.valid.id, userId: authStub.user1.user.id, allowDownload: false, @@ -232,31 +214,31 @@ describe(SharedLinkService.name, () => { describe('remove', () => { it('should throw an error for an invalid shared link', async () => { - shareMock.get.mockResolvedValue(null); + sharedLinkMock.get.mockResolvedValue(null); await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); - expect(shareMock.update).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); + expect(sharedLinkMock.update).not.toHaveBeenCalled(); }); it('should remove a key', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); await sut.remove(authStub.user1, sharedLinkStub.valid.id); - expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); - expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid); + expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); + expect(sharedLinkMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid); }); }); describe('addAssets', () => { it('should not work on album shared links', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( BadRequestException, ); }); it('should add assets to a shared link', async () => { - shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - shareMock.create.mockResolvedValue(sharedLinkStub.individual); + sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); + sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); await expect( @@ -268,7 +250,7 @@ describe(SharedLinkService.name, () => { ]); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1); - expect(shareMock.update).toHaveBeenCalledWith({ + expect(sharedLinkMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [assetStub.image, { id: 'asset-3' }], }); @@ -277,15 +259,15 @@ describe(SharedLinkService.name, () => { describe('removeAssets', () => { it('should not work on album shared links', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.valid); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid); await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( BadRequestException, ); }); it('should remove assets from a shared link', async () => { - shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - shareMock.create.mockResolvedValue(sharedLinkStub.individual); + sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); + sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual); await expect( sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }), @@ -294,29 +276,29 @@ describe(SharedLinkService.name, () => { { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, ]); - expect(shareMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] }); + expect(sharedLinkMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] }); }); }); describe('getMetadataTags', () => { it('should return null when auth is not a shared link', async () => { await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null); - expect(shareMock.get).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).not.toHaveBeenCalled(); }); it('should return null when shared link has a password', async () => { await expect(sut.getMetadataTags(authStub.passwordSharedLink)).resolves.toBe(null); - expect(shareMock.get).not.toHaveBeenCalled(); + expect(sharedLinkMock.get).not.toHaveBeenCalled(); }); it('should return metadata tags', async () => { - shareMock.get.mockResolvedValue(sharedLinkStub.individual); + sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual); await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '1 shared photos & videos', imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, title: 'Public Share', }); - expect(shareMock.get).toHaveBeenCalled(); + expect(sharedLinkMock.get).toHaveBeenCalled(); }); }); }); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index f2b0ea3c659ab..3116f0554be46 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; @@ -14,32 +14,14 @@ import { import { AssetEntity } from 'src/entities/asset.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { Permission, SharedLinkType } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(SharedLinkService.name); - } - getAll(auth: AuthDto): Promise { - return this.repository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link))); + return this.sharedLinkRepository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link))); } async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { @@ -67,7 +49,7 @@ export class SharedLinkService extends BaseService { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); break; } @@ -76,13 +58,13 @@ export class SharedLinkService extends BaseService { throw new BadRequestException('Invalid assetIds'); } - await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); break; } } - const sharedLink = await this.repository.create({ + const sharedLink = await this.sharedLinkRepository.create({ key: this.cryptoRepository.randomBytes(50), userId: auth.user.id, type: dto.type, @@ -101,7 +83,7 @@ export class SharedLinkService extends BaseService { async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) { await this.findOrFail(auth.user.id, id); - const sharedLink = await this.repository.update({ + const sharedLink = await this.sharedLinkRepository.update({ id, userId: auth.user.id, description: dto.description, @@ -116,12 +98,12 @@ export class SharedLinkService extends BaseService { async remove(auth: AuthDto, id: string): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - await this.repository.remove(sharedLink); + await this.sharedLinkRepository.remove(sharedLink); } // TODO: replace `userId` with permissions and access control checks private async findOrFail(userId: string, id: string) { - const sharedLink = await this.repository.get(userId, id); + const sharedLink = await this.sharedLinkRepository.get(userId, id); if (!sharedLink) { throw new BadRequestException('Shared link not found'); } @@ -137,7 +119,7 @@ export class SharedLinkService extends BaseService { const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); - const allowedAssetIds = await checkAccess(this.access, { + const allowedAssetIds = await checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_SHARE, ids: notPresentAssetIds, @@ -161,7 +143,7 @@ export class SharedLinkService extends BaseService { sharedLink.assets.push({ id: assetId } as AssetEntity); } - await this.repository.update(sharedLink); + await this.sharedLinkRepository.update(sharedLink); return results; } @@ -185,7 +167,7 @@ export class SharedLinkService extends BaseService { sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId); } - await this.repository.update(sharedLink); + await this.sharedLinkRepository.update(sharedLink); return results; } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index be9fab54c69d3..1caf66e56216a 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,9 +1,6 @@ import { SystemConfig } from 'src/config'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @@ -11,47 +8,20 @@ import { SmartInfoService } from 'src/services/smart-info.service'; import { getCLIPModelInfo } from 'src/utils/misc'; import { assetStub } from 'test/fixtures/asset.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; -import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(SmartInfoService.name, () => { let sut: SmartInfoService; + let assetMock: Mocked; - let configMock: Mocked; - let systemMock: Mocked; let jobMock: Mocked; + let machineLearningMock: Mocked; let searchMock: Mocked; - let machineMock: Mocked; - let databaseMock: Mocked; - let loggerMock: Mocked; + let systemMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); - configMock = newConfigRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - searchMock = newSearchRepositoryMock(); - jobMock = newJobRepositoryMock(); - machineMock = newMachineLearningRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new SmartInfoService( - assetMock, - configMock, - databaseMock, - jobMock, - machineMock, - searchMock, - systemMock, - loggerMock, - ); + ({ sut, assetMock, jobMock, machineLearningMock, searchMock, systemMock } = newTestService(SmartInfoService)); assetMock.getByIds.mockResolvedValue([assetStub.image]); }); @@ -313,7 +283,7 @@ describe(SmartInfoService.name, () => { expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); - expect(machineMock.encodeImage).not.toHaveBeenCalled(); + expect(machineLearningMock.encodeImage).not.toHaveBeenCalled(); }); it('should skip assets without a resize path', async () => { @@ -322,15 +292,15 @@ describe(SmartInfoService.name, () => { expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED); expect(searchMock.upsert).not.toHaveBeenCalled(); - expect(machineMock.encodeImage).not.toHaveBeenCalled(); + expect(machineLearningMock.encodeImage).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { - machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); + machineLearningMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); - expect(machineMock.encodeImage).toHaveBeenCalledWith( + expect(machineLearningMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), @@ -343,7 +313,7 @@ describe(SmartInfoService.name, () => { expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); - expect(machineMock.encodeImage).not.toHaveBeenCalled(); + expect(machineLearningMock.encodeImage).not.toHaveBeenCalled(); expect(searchMock.upsert).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 6f24dafbfee52..66c6499940b2f 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,23 +1,17 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { WithoutProperty } from 'src/interfaces/asset.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, - IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { ISearchRepository } from 'src/interfaces/search.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; @@ -25,20 +19,6 @@ import { usePagination } from 'src/utils/pagination'; @Injectable() export class SmartInfoService extends BaseService { - constructor( - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository, - @Inject(ISearchRepository) private repository: ISearchRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(SmartInfoService.name); - } - @OnEvent({ name: 'app.bootstrap' }) async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { @@ -72,7 +52,7 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => { const { dimSize } = getCLIPModelInfo(newConfig.machineLearning.clip.modelName); - const dbDimSize = await this.repository.getDimensionSize(); + const dbDimSize = await this.searchRepository.getDimensionSize(); this.logger.verbose(`Current database CLIP dimension size is ${dbDimSize}`); const modelChange = @@ -93,10 +73,10 @@ export class SmartInfoService extends BaseService { `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`, ); this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); - await this.repository.setDimensionSize(dimSize); + await this.searchRepository.setDimensionSize(dimSize); this.logger.log(`Successfully updated database CLIP dimension size from ${dbDimSize} to ${dimSize}.`); } else { - await this.repository.deleteAllSearchEmbeddings(); + await this.searchRepository.deleteAllSearchEmbeddings(); } if (!isPaused) { @@ -112,7 +92,7 @@ export class SmartInfoService extends BaseService { } if (force) { - await this.repository.deleteAllSearchEmbeddings(); + await this.searchRepository.deleteAllSearchEmbeddings(); } const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { @@ -150,7 +130,7 @@ export class SmartInfoService extends BaseService { return JobStatus.FAILED; } - const embedding = await this.machineLearning.encodeImage( + const embedding = await this.machineLearningRepository.encodeImage( machineLearning.url, previewFile.path, machineLearning.clip, @@ -161,7 +141,7 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.wait(DatabaseLock.CLIPDimSize); } - await this.repository.upsert(asset.id, embedding); + await this.searchRepository.upsert(asset.id, embedding); return JobStatus.SUCCESS; } diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 29a598d4b413a..c965d3e73e885 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -1,21 +1,13 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { IStackRepository } from 'src/interfaces/stack.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; @Injectable() -export class StackService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IStackRepository) private stackRepository: IStackRepository, - ) {} - +export class StackService extends BaseService { async search(auth: AuthDto, dto: StackSearchDto): Promise { const stacks = await this.stackRepository.search({ ownerId: auth.user.id, @@ -26,7 +18,7 @@ export class StackService { } async create(auth: AuthDto, dto: StackCreateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); @@ -36,13 +28,13 @@ export class StackService { } async get(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_READ, ids: [id] }); const stack = await this.findOrFail(id); return mapStack(stack, { auth }); } async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_UPDATE, ids: [id] }); const stack = await this.findOrFail(id); if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { throw new BadRequestException('Primary asset must be in the stack'); @@ -56,13 +48,13 @@ export class StackService { } async delete(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: [id] }); await this.stackRepository.delete(id); await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id }); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); + await requireAccess(this.accessRepository, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id }); } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index aa127c2afc64b..6e5af3baf9723 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -4,13 +4,9 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; @@ -18,66 +14,30 @@ import { StorageTemplateService } from 'src/services/storage-template.service'; import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; -import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(StorageTemplateService.name, () => { let sut: StorageTemplateService; + let albumMock: Mocked; let assetMock: Mocked; - let configMock: Mocked; let cryptoMock: Mocked; - let databaseMock: Mocked; let moveMock: Mocked; - let personMock: Mocked; let storageMock: Mocked; let systemMock: Mocked; let userMock: Mocked; - let loggerMock: Mocked; it('should work', () => { expect(sut).toBeDefined(); }); beforeEach(() => { - assetMock = newAssetRepositoryMock(); - albumMock = newAlbumRepositoryMock(); - configMock = newConfigRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - moveMock = newMoveRepositoryMock(); - personMock = newPersonRepositoryMock(); - storageMock = newStorageRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); + ({ sut, albumMock, assetMock, cryptoMock, moveMock, storageMock, systemMock, userMock } = + newTestService(StorageTemplateService)); systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } }); - sut = new StorageTemplateService( - albumMock, - assetMock, - configMock, - systemMock, - moveMock, - personMock, - storageMock, - userMock, - cryptoMock, - databaseMock, - loggerMock, - ); - sut.onConfigUpdate({ newConfig: defaults }); }); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index c0bf11b186a28..e400981f541c5 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import handlebar from 'handlebars'; import { DateTime } from 'luxon'; import path from 'node:path'; @@ -16,19 +16,9 @@ import { StorageCore } from 'src/cores/storage.core'; import { OnEvent } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, AssetType, StorageFolder } from 'src/enum'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMoveRepository } from 'src/interfaces/move.interface'; -import { IPersonRepository } from 'src/interfaces/person.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; import { getLivePhotoMotionFilename } from 'src/utils/file'; import { usePagination } from 'src/utils/pagination'; @@ -47,7 +37,6 @@ interface RenderMetadata { @Injectable() export class StorageTemplateService extends BaseService { - private storageCore: StorageCore; private _template: { compiled: HandlebarsTemplateDelegate; raw: string; @@ -61,33 +50,6 @@ export class StorageTemplateService extends BaseService { return this._template; } - constructor( - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IMoveRepository) moveRepository: IMoveRepository, - @Inject(IPersonRepository) personRepository: IPersonRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(StorageTemplateService.name); - this.storageCore = StorageCore.create( - assetRepository, - configRepository, - cryptoRepository, - moveRepository, - personRepository, - storageRepository, - systemMetadataRepository, - this.logger, - ); - } - @OnEvent({ name: 'config.update', server: true }) onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { const template = newConfig.storageTemplate.template; diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index e0717df66860e..45a18d8c4bdb6 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -1,33 +1,21 @@ import { SystemMetadataKey } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; -import { IDatabaseRepository } from 'src/interfaces/database.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { StorageService } from 'src/services/storage.service'; -import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(StorageService.name, () => { let sut: StorageService; + let configMock: Mocked; - let databaseMock: Mocked; let storageMock: Mocked; - let loggerMock: Mocked; let systemMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - storageMock = newStorageRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - - sut = new StorageService(configMock, databaseMock, storageMock, loggerMock, systemMock); + ({ sut, configMock, storageMock, systemMock } = newTestService(StorageService)); }); it('should work', () => { diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index e2c1ef28e20ed..15868d646d4df 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,36 +1,23 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { join } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent } from 'src/decorators'; import { StorageFolder, SystemMetadataKey } from 'src/enum'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; import { ImmichStartupError } from 'src/utils/events'; const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`; @Injectable() -export class StorageService { - constructor( - @Inject(IConfigRepository) private configRepository: IConfigRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - @Inject(ISystemMetadataRepository) private systemMetadata: ISystemMetadataRepository, - ) { - this.logger.setContext(StorageService.name); - } - +export class StorageService extends BaseService { @OnEvent({ name: 'app.bootstrap' }) async onBootstrap() { const envData = this.configRepository.getEnv(); await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { - const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false }; + const flags = (await this.systemMetadataRepository.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false }; const enabled = flags.mountFiles ?? false; this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`); @@ -49,7 +36,7 @@ export class StorageService { if (!flags.mountFiles) { flags.mountFiles = true; - await this.systemMetadata.set(SystemMetadataKey.SYSTEM_FLAGS, flags); + await this.systemMetadataRepository.set(SystemMetadataKey.SYSTEM_FLAGS, flags); this.logger.log('Successfully enabled system mount folders checks'); } diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index a0ded6dba3626..8dc270d020555 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -1,6 +1,5 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; @@ -8,10 +7,7 @@ import { SyncService } from 'src/services/sync.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; -import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; const untilDate = new Date(2024); @@ -19,17 +15,13 @@ const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: tr describe(SyncService.name, () => { let sut: SyncService; - let accessMock: Mocked; + let assetMock: Mocked; - let partnerMock: Mocked; let auditMock: Mocked; + let partnerMock: Mocked; beforeEach(() => { - partnerMock = newPartnerRepositoryMock(); - assetMock = newAssetRepositoryMock(); - accessMock = newAccessRepositoryMock(); - auditMock = newAuditRepositoryMock(); - sut = new SyncService(accessMock, assetMock, partnerMock, auditMock); + ({ sut, assetMock, auditMock, partnerMock } = newTestService(SyncService)); }); it('should exist', () => { diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 7da3fbd9be58d..e09c06c778a08 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,32 +1,21 @@ -import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; import { DatabaseAction, EntityType, Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IAuditRepository } from 'src/interfaces/audit.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { setIsEqual } from 'src/utils/set'; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; -export class SyncService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(IAuditRepository) private auditRepository: IAuditRepository, - ) {} - +export class SyncService extends BaseService { async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; - await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, updatedUntil: dto.updatedUntil, @@ -50,7 +39,7 @@ export class SyncService { return FULL_SYNC; } - await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 5782443c758b2..52a5b1dcd8b43 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -18,10 +18,8 @@ import { QueueName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SystemConfigService } from 'src/services/system-config.service'; -import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService } from 'test/utils'; import { DeepPartial } from 'typeorm'; import { Mocked } from 'vitest'; @@ -189,18 +187,14 @@ const updatedConfig = Object.freeze({ describe(SystemConfigService.name, () => { let sut: SystemConfigService; + let configMock: Mocked; - let systemMock: Mocked; let eventMock: Mocked; let loggerMock: Mocked; + let systemMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - eventMock = newEventRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new SystemConfigService(configMock, eventMock, systemMock, loggerMock); + ({ sut, configMock, eventMock, loggerMock, systemMock } = newTestService(SystemConfigService)); }); it('should work', () => { diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index ff749f1105546..96a1f0897bb36 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; import { defaults } from 'src/config'; @@ -14,26 +14,13 @@ import { } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { BaseService } from 'src/services/base.service'; import { clearConfigCache } from 'src/utils/config'; import { toPlainObject } from 'src/utils/object'; @Injectable() export class SystemConfigService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(SystemConfigService.name); - } - @OnEvent({ name: 'app.bootstrap', priority: -100 }) async onBootstrap() { const config = await this.getConfig({ withCache: false }); diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts index 5799ee859d8c6..f17b398ab1471 100644 --- a/server/src/services/system-metadata.service.spec.ts +++ b/server/src/services/system-metadata.service.spec.ts @@ -1,16 +1,15 @@ import { SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SystemMetadataService } from 'src/services/system-metadata.service'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(SystemMetadataService.name, () => { let sut: SystemMetadataService; - let metadataMock: Mocked; + let systemMock: Mocked; beforeEach(() => { - metadataMock = newSystemMetadataRepositoryMock(); - sut = new SystemMetadataService(metadataMock); + ({ sut, systemMock } = newTestService(SystemMetadataService)); }); it('should work', () => { @@ -20,12 +19,12 @@ describe(SystemMetadataService.name, () => { describe('updateAdminOnboarding', () => { it('should update isOnboarded to true', async () => { await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); - expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); + expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true }); }); it('should update isOnboarded to false', async () => { await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); - expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); + expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false }); }); }); }); diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts index c2c9a4fdfc8c2..93449c7a7b5d7 100644 --- a/server/src/services/system-metadata.service.ts +++ b/server/src/services/system-metadata.service.ts @@ -1,29 +1,27 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { AdminOnboardingResponseDto, AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto, } from 'src/dtos/system-metadata.dto'; import { SystemMetadataKey } from 'src/enum'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { BaseService } from 'src/services/base.service'; @Injectable() -export class SystemMetadataService { - constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {} - +export class SystemMetadataService extends BaseService { async getAdminOnboarding(): Promise { - const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); + const value = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING); return { isOnboarded: false, ...value }; } async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise { - await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { + await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: dto.isOnboarded, }); } async getReverseGeocodingState(): Promise { - const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + const value = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); return { lastUpdate: null, lastImportFileName: null, ...value }; } } diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index a479a09fbb48e..69840a83da599 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -1,26 +1,21 @@ import { BadRequestException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; import { TagService } from 'src/services/tag.service'; import { authStub } from 'test/fixtures/auth.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(TagService.name, () => { let sut: TagService; + let accessMock: IAccessRepositoryMock; - let eventMock: Mocked; let tagMock: Mocked; beforeEach(() => { - accessMock = newAccessRepositoryMock(); - eventMock = newEventRepositoryMock(); - tagMock = newTagRepositoryMock(); - sut = new TagService(accessMock, eventMock, tagMock); + ({ sut, accessMock, tagMock } = newTestService(TagService)); accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index cc6d64f749d20..2824a9832ddea 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { @@ -12,29 +12,22 @@ import { } from 'src/dtos/tag.dto'; import { TagEntity } from 'src/entities/tag.entity'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { JobStatus } from 'src/interfaces/job.interface'; -import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; +import { AssetTagItem } from 'src/interfaces/tag.interface'; +import { BaseService } from 'src/services/base.service'; import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { upsertTags } from 'src/utils/tag'; @Injectable() -export class TagService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(ITagRepository) private repository: ITagRepository, - ) {} - +export class TagService extends BaseService { async getAll(auth: AuthDto) { - const tags = await this.repository.getAll(auth.user.id); + const tags = await this.tagRepository.getAll(auth.user.id); return tags.map((tag) => mapTag(tag)); } async get(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [id] }); const tag = await this.findOrFail(id); return mapTag(tag); } @@ -42,8 +35,8 @@ export class TagService { async create(auth: AuthDto, dto: TagCreateDto) { let parent: TagEntity | undefined; if (dto.parentId) { - await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); - parent = (await this.repository.get(dto.parentId)) || undefined; + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); + parent = (await this.tagRepository.get(dto.parentId)) || undefined; if (!parent) { throw new BadRequestException('Tag not found'); } @@ -51,41 +44,41 @@ export class TagService { const userId = auth.user.id; const value = parent ? `${parent.value}/${dto.name}` : dto.name; - const duplicate = await this.repository.getByValue(userId, value); + const duplicate = await this.tagRepository.getByValue(userId, value); if (duplicate) { throw new BadRequestException(`A tag with that name already exists`); } - const tag = await this.repository.create({ userId, value, parent }); + const tag = await this.tagRepository.create({ userId, value, parent }); return mapTag(tag); } async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.TAG_UPDATE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_UPDATE, ids: [id] }); const { color } = dto; - const tag = await this.repository.update({ id, color }); + const tag = await this.tagRepository.update({ id, color }); return mapTag(tag); } async upsert(auth: AuthDto, dto: TagUpsertDto) { - const tags = await upsertTags(this.repository, { userId: auth.user.id, tags: dto.tags }); + const tags = await upsertTags(this.tagRepository, { userId: auth.user.id, tags: dto.tags }); return tags.map((tag) => mapTag(tag)); } async remove(auth: AuthDto, id: string): Promise { - await requireAccess(this.access, { auth, permission: Permission.TAG_DELETE, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_DELETE, ids: [id] }); // TODO sync tag changes for affected assets - await this.repository.delete(id); + await this.tagRepository.delete(id); } async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise { const [tagIds, assetIds] = await Promise.all([ - checkAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), - checkAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), + checkAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), + checkAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), ]); const items: AssetTagItem[] = []; @@ -95,7 +88,7 @@ export class TagService { } } - const results = await this.repository.upsertAssetIds(items); + const results = await this.tagRepository.upsertAssetIds(items); for (const assetId of new Set(results.map((item) => item.assetId))) { await this.eventRepository.emit('asset.tag', { assetId }); } @@ -104,11 +97,11 @@ export class TagService { } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] }); const results = await addAssets( auth, - { access: this.access, bulk: this.repository }, + { access: this.accessRepository, bulk: this.tagRepository }, { parentId: id, assetIds: dto.ids }, ); @@ -122,11 +115,11 @@ export class TagService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_ASSET, ids: [id] }); const results = await removeAssets( auth, - { access: this.access, bulk: this.repository }, + { access: this.accessRepository, bulk: this.tagRepository }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE }, ); @@ -140,12 +133,12 @@ export class TagService { } async handleTagCleanup() { - await this.repository.deleteEmptyTags(); + await this.tagRepository.deleteEmptyTags(); return JobStatus.SUCCESS; } private async findOrFail(id: string) { - const tag = await this.repository.get(id); + const tag = await this.tagRepository.get(id); if (!tag) { throw new BadRequestException('Tag not found'); } diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 981fc11c3f5ab..3de7f71fae400 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -1,25 +1,20 @@ import { BadRequestException } from '@nestjs/common'; import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { TimelineService } from 'src/services/timeline.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; -import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(TimelineService.name, () => { let sut: TimelineService; + let accessMock: IAccessRepositoryMock; let assetMock: Mocked; - let partnerMock: Mocked; - beforeEach(() => { - accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); - partnerMock = newPartnerRepositoryMock(); - sut = new TimelineService(accessMock, assetMock, partnerMock); + beforeEach(() => { + ({ sut, accessMock, assetMock } = newTestService(TimelineService)); }); describe('getTimeBuckets', () => { diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index bc08505b944eb..48e4daafd1096 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,26 +1,18 @@ -import { BadRequestException, Inject } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; -import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { TimeBucketOptions } from 'src/interfaces/asset.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; -export class TimelineService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private repository: IAssetRepository, - @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - ) {} - +export class TimelineService extends BaseService { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - - return this.repository.getTimeBuckets(timeBucketOptions); + return this.assetRepository.getTimeBuckets(timeBucketOptions); } async getTimeBucket( @@ -29,7 +21,7 @@ export class TimelineService { ): Promise { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - const assets = await this.repository.getTimeBucket(dto.timeBucket, timeBucketOptions); + const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); return !auth.sharedLink || auth.sharedLink?.showExif ? assets.map((asset) => mapAsset(asset, { withStack: true, auth })) : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth })); @@ -56,20 +48,20 @@ export class TimelineService { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (dto.albumId) { - await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); if (dto.isArchived !== false) { - await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); } } if (dto.tagId) { - await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] }); + await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] }); } if (dto.withPartners) { diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index d0c719ae48e73..748faa14abdc2 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,37 +1,25 @@ import { BadRequestException } from '@nestjs/common'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ITrashRepository } from 'src/interfaces/trash.interface'; import { TrashService } from 'src/services/trash.service'; import { authStub } from 'test/fixtures/auth.stub'; -import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock'; +import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; describe(TrashService.name, () => { let sut: TrashService; + let accessMock: IAccessRepositoryMock; - let eventMock: Mocked; let jobMock: Mocked; let trashMock: Mocked; - let loggerMock: Mocked; it('should work', () => { expect(sut).toBeDefined(); }); beforeEach(() => { - accessMock = newAccessRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - trashMock = newTrashRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new TrashService(accessMock, eventMock, jobMock, trashMock, loggerMock); + ({ sut, accessMock, jobMock, trashMock } = newTestService(TrashService)); }); describe('restoreAssets', () => { diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 51771d38a2aa9..add6e29f6bb14 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,35 +1,21 @@ -import { Inject } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TrashResponseDto } from 'src/dtos/trash.dto'; import { Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { BaseService } from 'src/services/base.service'; import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; -export class TrashService { - constructor( - @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(ITrashRepository) private trashRepository: ITrashRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - ) { - this.logger.setContext(TrashService.name); - } - +export class TrashService extends BaseService { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; if (ids.length === 0) { return { count: 0 }; } - await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); + await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids }); await this.trashRepository.restoreAll(ids); await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id }); diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 8e80aa4dc109a..70999332dc26a 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -1,41 +1,22 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { mapUserAdmin } from 'src/dtos/user.dto'; import { UserStatus } from 'src/enum'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { UserAdminService } from 'src/services/user-admin.service'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked, describe } from 'vitest'; describe(UserAdminService.name, () => { let sut: UserAdminService; - let albumMock: Mocked; - let cryptoMock: Mocked; - let eventMock: Mocked; + let jobMock: Mocked; - let loggerMock: Mocked; let userMock: Mocked; beforeEach(() => { - albumMock = newAlbumRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new UserAdminService(albumMock, cryptoMock, eventMock, jobMock, userMock, loggerMock); + ({ sut, jobMock, userMock } = newTestService(UserAdminService)); userMock.get.mockImplementation((userId) => Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 75dff32f160a9..94608a24ac035 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { SALT_ROUNDS } from 'src/constants'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; @@ -11,28 +11,14 @@ import { mapUserAdmin, } from 'src/dtos/user.dto'; import { UserMetadataKey, UserStatus } from 'src/enum'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; +import { JobName } from 'src/interfaces/job.interface'; +import { UserFindOptions } from 'src/interfaces/user.interface'; +import { BaseService } from 'src/services/base.service'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { createUser } from 'src/utils/user'; @Injectable() -export class UserAdminService { - constructor( - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) private logger: ILoggerRepository, - ) { - this.logger.setContext(UserAdminService.name); - } - +export class UserAdminService extends BaseService { async search(auth: AuthDto, dto: UserAdminSearchDto): Promise { const users = await this.userRepository.getList({ withDeleted: dto.withDeleted }); return users.map((user) => mapUserAdmin(user)); diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 4a121dfda2c76..767d8d895453a 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -2,10 +2,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException } import { UserEntity } from 'src/entities/user.entity'; import { CacheControl, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; @@ -14,14 +11,7 @@ import { ImmichFileResponse } from 'src/utils/file'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; -import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; -import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; const makeDeletedAt = (daysAgo: number) => { @@ -32,36 +22,15 @@ const makeDeletedAt = (daysAgo: number) => { describe(UserService.name, () => { let sut: UserService; - let userMock: Mocked; - let cryptoRepositoryMock: Mocked; let albumMock: Mocked; - let configMock: Mocked; let jobMock: Mocked; let storageMock: Mocked; let systemMock: Mocked; - let loggerMock: Mocked; + let userMock: Mocked; beforeEach(() => { - albumMock = newAlbumRepositoryMock(); - configMock = newConfigRepositoryMock(); - cryptoRepositoryMock = newCryptoRepositoryMock(); - jobMock = newJobRepositoryMock(); - storageMock = newStorageRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - userMock = newUserRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new UserService( - albumMock, - configMock, - cryptoRepositoryMock, - jobMock, - storageMock, - systemMock, - userMock, - loggerMock, - ); + ({ sut, albumMock, jobMock, storageMock, systemMock, userMock } = newTestService(UserService)); userMock.get.mockImplementation((userId) => Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 92c9c29994494..5472c579f37cc 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { DateTime } from 'luxon'; import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; @@ -11,34 +11,14 @@ import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUse import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum'; -import { IAlbumRepository } from 'src/interfaces/album.interface'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IStorageRepository } from 'src/interfaces/storage.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; +import { IEntityJob, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { UserFindOptions } from 'src/interfaces/user.interface'; import { BaseService } from 'src/services/base.service'; import { ImmichFileResponse } from 'src/utils/file'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() export class UserService extends BaseService { - constructor( - @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(UserService.name); - } - async search(): Promise { const users = await this.userRepository.getList({ withDeleted: false }); return users.map((user) => mapUser(user)); diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index ebc5d4b2322f8..70a58059d1a10 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -2,7 +2,6 @@ import { DateTime } from 'luxon'; import { serverVersion } from 'src/constants'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { IConfigRepository } from 'src/interfaces/config.interface'; -import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -10,14 +9,8 @@ import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { VersionService } from 'src/services/version.service'; -import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; -import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; -import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; -import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; -import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; -import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; const mockRelease = (version: string) => ({ @@ -32,35 +25,18 @@ const mockRelease = (version: string) => ({ describe(VersionService.name, () => { let sut: VersionService; + let configMock: Mocked; - let databaseMock: Mocked; let eventMock: Mocked; let jobMock: Mocked; - let serverMock: Mocked; - let systemMock: Mocked; - let versionMock: Mocked; let loggerMock: Mocked; + let serverInfoMock: Mocked; + let systemMock: Mocked; + let versionHistoryMock: Mocked; beforeEach(() => { - configMock = newConfigRepositoryMock(); - databaseMock = newDatabaseRepositoryMock(); - eventMock = newEventRepositoryMock(); - jobMock = newJobRepositoryMock(); - serverMock = newServerInfoRepositoryMock(); - systemMock = newSystemMetadataRepositoryMock(); - versionMock = newVersionHistoryRepositoryMock(); - loggerMock = newLoggerRepositoryMock(); - - sut = new VersionService( - configMock, - databaseMock, - eventMock, - jobMock, - serverMock, - systemMock, - versionMock, - loggerMock, - ); + ({ sut, configMock, eventMock, jobMock, loggerMock, serverInfoMock, systemMock, versionHistoryMock } = + newTestService(VersionService)); }); it('should work', () => { @@ -70,17 +46,17 @@ describe(VersionService.name, () => { describe('onBootstrap', () => { it('should record a new version', async () => { await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(versionMock.create).toHaveBeenCalledWith({ version: expect.any(String) }); + expect(versionHistoryMock.create).toHaveBeenCalledWith({ version: expect.any(String) }); }); it('should skip a duplicate version', async () => { - versionMock.getLatest.mockResolvedValue({ + versionHistoryMock.getLatest.mockResolvedValue({ id: 'version-1', createdAt: new Date(), version: serverVersion.toString(), }); await expect(sut.onBootstrap()).resolves.toBeUndefined(); - expect(versionMock.create).not.toHaveBeenCalled(); + expect(versionHistoryMock.create).not.toHaveBeenCalled(); }); }); @@ -97,7 +73,7 @@ describe(VersionService.name, () => { describe('getVersionHistory', () => { it('should respond the server version history', async () => { const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' }; - versionMock.getAll.mockResolvedValue([upgrade]); + versionHistoryMock.getAll.mockResolvedValue([upgrade]); await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]); }); }); @@ -128,7 +104,7 @@ describe(VersionService.name, () => { }); it('should run if it has been > 60 minutes', async () => { - serverMock.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0')); + serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease('v100.0.0')); systemMock.get.mockResolvedValue({ checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(), releaseVersion: '1.0.0', @@ -140,7 +116,7 @@ describe(VersionService.name, () => { }); it('should not notify if the version is equal', async () => { - serverMock.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString())); + serverInfoMock.getGitHubRelease.mockResolvedValue(mockRelease(serverVersion.toString())); await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS); expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, { checkedAt: expect.any(String), @@ -150,7 +126,7 @@ describe(VersionService.name, () => { }); it('should handle a github error', async () => { - serverMock.getGitHubRelease.mockRejectedValue(new Error('GitHub is down')); + serverInfoMock.getGitHubRelease.mockRejectedValue(new Error('GitHub is down')); await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.FAILED); expect(systemMock.set).not.toHaveBeenCalled(); expect(eventMock.clientBroadcast).not.toHaveBeenCalled(); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 60ea388e5daf1..231ced1a950af 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; @@ -6,14 +6,9 @@ import { OnEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; -import { IConfigRepository } from 'src/interfaces/config.interface'; -import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; -import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; -import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; +import { DatabaseLock } from 'src/interfaces/database.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; +import { JobName, JobStatus } from 'src/interfaces/job.interface'; import { BaseService } from 'src/services/base.service'; const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { @@ -27,20 +22,6 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re @Injectable() export class VersionService extends BaseService { - constructor( - @Inject(IConfigRepository) configRepository: IConfigRepository, - @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, - @Inject(IEventRepository) private eventRepository: IEventRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, - @Inject(IServerInfoRepository) private repository: IServerInfoRepository, - @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, - @Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository, - @Inject(ILoggerRepository) logger: ILoggerRepository, - ) { - super(configRepository, systemMetadataRepository, logger); - this.logger.setContext(VersionService.name); - } - @OnEvent({ name: 'app.bootstrap' }) async onBootstrap(): Promise { await this.handleVersionCheck(); @@ -91,7 +72,8 @@ export class VersionService extends BaseService { } } - const { tag_name: releaseVersion, published_at: publishedAt } = await this.repository.getGitHubRelease(); + const { tag_name: releaseVersion, published_at: publishedAt } = + await this.serverInfoRepository.getGitHubRelease(); const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion }; await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata); diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts index 8d17e4d897401..e9373ce66fb7f 100644 --- a/server/src/services/view.service.spec.ts +++ b/server/src/services/view.service.spec.ts @@ -3,7 +3,7 @@ import { IViewRepository } from 'src/interfaces/view.interface'; import { ViewService } from 'src/services/view.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { newViewRepositoryMock } from 'test/repositories/view.repository.mock'; +import { newTestService } from 'test/utils'; import { Mocked } from 'vitest'; @@ -12,9 +12,7 @@ describe(ViewService.name, () => { let viewMock: Mocked; beforeEach(() => { - viewMock = newViewRepositoryMock(); - - sut = new ViewService(viewMock); + ({ sut, viewMock } = newTestService(ViewService)); }); it('should work', () => { diff --git a/server/src/services/view.service.ts b/server/src/services/view.service.ts index d870f9fd2e1a6..cb805368705df 100644 --- a/server/src/services/view.service.ts +++ b/server/src/services/view.service.ts @@ -1,11 +1,8 @@ -import { Inject } from '@nestjs/common'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { IViewRepository } from 'src/interfaces/view.interface'; - -export class ViewService { - constructor(@Inject(IViewRepository) private viewRepository: IViewRepository) {} +import { BaseService } from 'src/services/base.service'; +export class ViewService extends BaseService { getUniqueOriginalPaths(auth: AuthDto): Promise { return this.viewRepository.getUniqueOriginalPaths(auth.user.id); } diff --git a/server/test/utils.ts b/server/test/utils.ts new file mode 100644 index 0000000000000..c744443bd6a70 --- /dev/null +++ b/server/test/utils.ts @@ -0,0 +1,160 @@ +import { BaseService } from 'src/services/base.service'; +import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; +import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; +import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; +import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; +import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newAuditRepositoryMock } from 'test/repositories/audit.repository.mock'; +import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; +import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; +import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; +import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; +import { newMapRepositoryMock } from 'test/repositories/map.repository.mock'; +import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; +import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; +import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; +import { newMetricRepositoryMock } from 'test/repositories/metric.repository.mock'; +import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; +import { newNotificationRepositoryMock } from 'test/repositories/notification.repository.mock'; +import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; +import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; +import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; +import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; +import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock'; +import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; +import { newStackRepositoryMock } from 'test/repositories/stack.repository.mock'; +import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; +import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; +import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock'; +import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; +import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock'; +import { newViewRepositoryMock } from 'test/repositories/view.repository.mock'; + +type BaseServiceArgs = ConstructorParameters; +type Constructor> = { + new (...deps: Args): Type; +}; + +export const newTestService = (Service: Constructor) => { + const accessMock = newAccessRepositoryMock(); + const loggerMock = newLoggerRepositoryMock(); + const cryptoMock = newCryptoRepositoryMock(); + const activityMock = newActivityRepositoryMock(); + const auditMock = newAuditRepositoryMock(); + const albumMock = newAlbumRepositoryMock(); + const albumUserMock = newAlbumUserRepositoryMock(); + const assetMock = newAssetRepositoryMock(); + const configMock = newConfigRepositoryMock(); + const databaseMock = newDatabaseRepositoryMock(); + const eventMock = newEventRepositoryMock(); + const jobMock = newJobRepositoryMock(); + const keyMock = newKeyRepositoryMock(); + const libraryMock = newLibraryRepositoryMock(); + const machineLearningMock = newMachineLearningRepositoryMock(); + const mapMock = newMapRepositoryMock(); + const mediaMock = newMediaRepositoryMock(); + const memoryMock = newMemoryRepositoryMock(); + const metadataMock = newMetadataRepositoryMock(); + const metricMock = newMetricRepositoryMock(); + const moveMock = newMoveRepositoryMock(); + const notificationMock = newNotificationRepositoryMock(); + const partnerMock = newPartnerRepositoryMock(); + const personMock = newPersonRepositoryMock(); + const searchMock = newSearchRepositoryMock(); + const serverInfoMock = newServerInfoRepositoryMock(); + const sessionMock = newSessionRepositoryMock(); + const sharedLinkMock = newSharedLinkRepositoryMock(); + const stackMock = newStackRepositoryMock(); + const storageMock = newStorageRepositoryMock(); + const systemMock = newSystemMetadataRepositoryMock(); + const tagMock = newTagRepositoryMock(); + const trashMock = newTrashRepositoryMock(); + const userMock = newUserRepositoryMock(); + const versionHistoryMock = newVersionHistoryRepositoryMock(); + const viewMock = newViewRepositoryMock(); + + const sut = new Service( + loggerMock, + accessMock, + activityMock, + auditMock, + albumMock, + albumUserMock, + assetMock, + configMock, + cryptoMock, + databaseMock, + eventMock, + jobMock, + keyMock, + libraryMock, + machineLearningMock, + mapMock, + mediaMock, + memoryMock, + metadataMock, + metricMock, + moveMock, + notificationMock, + partnerMock, + personMock, + searchMock, + serverInfoMock, + sessionMock, + sharedLinkMock, + stackMock, + storageMock, + systemMock, + tagMock, + trashMock, + userMock, + versionHistoryMock, + viewMock, + ); + + return { + sut, + accessMock, + loggerMock, + cryptoMock, + activityMock, + auditMock, + albumMock, + albumUserMock, + assetMock, + configMock, + databaseMock, + eventMock, + jobMock, + keyMock, + libraryMock, + machineLearningMock, + mapMock, + mediaMock, + memoryMock, + metadataMock, + metricMock, + moveMock, + notificationMock, + partnerMock, + personMock, + searchMock, + serverInfoMock, + sessionMock, + sharedLinkMock, + stackMock, + storageMock, + systemMock, + tagMock, + trashMock, + userMock, + versionHistoryMock, + viewMock, + }; +};