refactor: service dependencies (#13108)

refactor(server): simplify service dependency management
This commit is contained in:
Jason Rasmussen 2024-10-02 10:54:35 -04:00 committed by GitHub
parent 1b7e4b4e52
commit 4ea281f854
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 802 additions and 1862 deletions

View File

@ -4,20 +4,18 @@ import { IActivityRepository } from 'src/interfaces/activity.interface';
import { ActivityService } from 'src/services/activity.service'; import { ActivityService } from 'src/services/activity.service';
import { activityStub } from 'test/fixtures/activity.stub'; import { activityStub } from 'test/fixtures/activity.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock'; import { newTestService } from 'test/utils';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(ActivityService.name, () => { describe(ActivityService.name, () => {
let sut: ActivityService; let sut: ActivityService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let activityMock: Mocked<IActivityRepository>; let activityMock: Mocked<IActivityRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, activityMock } = newTestService(ActivityService));
activityMock = newActivityRepositoryMock();
sut = new ActivityService(accessMock, activityMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
ActivityCreateDto, ActivityCreateDto,
ActivityDto, ActivityDto,
@ -13,20 +13,14 @@ import {
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ActivityEntity } from 'src/entities/activity.entity'; import { ActivityEntity } from 'src/entities/activity.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { BaseService } from 'src/services/base.service';
import { IActivityRepository } from 'src/interfaces/activity.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class ActivityService { export class ActivityService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IActivityRepository) private repository: IActivityRepository,
) {}
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
const activities = await this.repository.search({ const activities = await this.activityRepository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId, assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId,
@ -37,12 +31,12 @@ export class ActivityService {
} }
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); await requireAccess(this.accessRepository, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] });
return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; return { comments: await this.activityRepository.getStatistics(dto.assetId, dto.albumId) };
} }
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> { async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
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 = { const common = {
userId: auth.user.id, userId: auth.user.id,
@ -55,7 +49,7 @@ export class ActivityService {
if (dto.type === ReactionType.LIKE) { if (dto.type === ReactionType.LIKE) {
delete dto.comment; delete dto.comment;
[activity] = await this.repository.search({ [activity] = await this.activityRepository.search({
...common, ...common,
// `null` will search for an album like // `null` will search for an album like
assetId: dto.assetId ?? null, assetId: dto.assetId ?? null,
@ -65,7 +59,7 @@ export class ActivityService {
} }
if (!activity) { if (!activity) {
activity = await this.repository.create({ activity = await this.activityRepository.create({
...common, ...common,
isLiked: dto.type === ReactionType.LIKE, isLiked: dto.type === ReactionType.LIKE,
comment: dto.comment, comment: dto.comment,
@ -76,7 +70,7 @@ export class ActivityService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] });
await this.repository.delete(id); await this.activityRepository.delete(id);
} }
} }

View File

@ -4,39 +4,27 @@ import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { AlbumUserRole } from 'src/enum'; import { AlbumUserRole } from 'src/enum';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AlbumService } from 'src/services/album.service'; import { AlbumService } from 'src/services/album.service';
import { albumStub } from 'test/fixtures/album.stub'; import { albumStub } from 'test/fixtures/album.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAlbumUserRepositoryMock } from 'test/repositories/album-user.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(AlbumService.name, () => { describe(AlbumService.name, () => {
let sut: AlbumService; let sut: AlbumService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let albumMock: Mocked<IAlbumRepository>; let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>; let albumUserMock: Mocked<IAlbumUserRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
let albumUserMock: Mocked<IAlbumUserRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, albumMock, albumUserMock, eventMock, userMock } = newTestService(AlbumService));
albumMock = newAlbumRepositoryMock();
assetMock = newAssetRepositoryMock();
eventMock = newEventRepositoryMock();
userMock = newUserRepositoryMock();
albumUserMock = newAlbumUserRepositoryMock();
sut = new AlbumService(accessMock, albumMock, assetMock, eventMock, userMock, albumUserMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { import {
AddUsersDto, AddUsersDto,
AlbumInfoDto, AlbumInfoDto,
@ -17,26 +17,13 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity';
import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { BaseService } from 'src/services/base.service';
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 { checkAccess, requireAccess } from 'src/utils/access'; import { checkAccess, requireAccess } from 'src/utils/access';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
@Injectable() @Injectable()
export class AlbumService { export class AlbumService extends BaseService {
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,
) {}
async getStatistics(auth: AuthDto): Promise<AlbumStatisticsResponseDto> { async getStatistics(auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
const [owned, shared, notShared] = await Promise.all([ const [owned, shared, notShared] = await Promise.all([
this.albumRepository.getOwned(auth.user.id), this.albumRepository.getOwned(auth.user.id),
@ -95,7 +82,7 @@ export class AlbumService {
} }
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
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(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets }); 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, auth,
permission: Permission.ASSET_SHARE, permission: Permission.ASSET_SHARE,
ids: dto.assetIds || [], ids: dto.assetIds || [],
@ -143,7 +130,7 @@ export class AlbumService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
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 }); const album = await this.findOrFail(id, { withAssets: true });
@ -166,17 +153,17 @@ export class AlbumService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
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); await this.albumRepository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); 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( const results = await addAssets(
auth, auth,
{ access: this.access, bulk: this.albumRepository }, { access: this.accessRepository, bulk: this.albumRepository },
{ parentId: id, assetIds: dto.ids }, { parentId: id, assetIds: dto.ids },
); );
@ -195,12 +182,12 @@ export class AlbumService {
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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 album = await this.findOrFail(id, { withAssets: false });
const results = await removeAssets( const results = await removeAssets(
auth, auth,
{ access: this.access, bulk: this.albumRepository }, { access: this.accessRepository, bulk: this.albumRepository },
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, { 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<AlbumResponseDto> { async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
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 }); const album = await this.findOrFail(id, { withAssets: false });
@ -260,14 +247,14 @@ export class AlbumService {
// non-admin can remove themselves // non-admin can remove themselves
if (auth.user.id !== userId) { 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 }); await this.albumUserRepository.delete({ albumId: id, userId });
} }
async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> { async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial<AlbumUserEntity>): Promise<void> {
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 }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
} }

View File

@ -5,19 +5,17 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { APIKeyService } from 'src/services/api-key.service'; import { APIKeyService } from 'src/services/api-key.service';
import { keyStub } from 'test/fixtures/api-key.stub'; import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newTestService } from 'test/utils';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(APIKeyService.name, () => { describe(APIKeyService.name, () => {
let sut: APIKeyService; let sut: APIKeyService;
let keyMock: Mocked<IKeyRepository>;
let cryptoMock: Mocked<ICryptoRepository>; let cryptoMock: Mocked<ICryptoRepository>;
let keyMock: Mocked<IKeyRepository>;
beforeEach(() => { beforeEach(() => {
cryptoMock = newCryptoRepositoryMock(); ({ sut, cryptoMock, keyMock } = newTestService(APIKeyService));
keyMock = newKeyRepositoryMock();
sut = new APIKeyService(cryptoMock, keyMock);
}); });
describe('create', () => { describe('create', () => {

View File

@ -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 { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { APIKeyEntity } from 'src/entities/api-key.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { BaseService } from 'src/services/base.service';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { isGranted } from 'src/utils/access'; import { isGranted } from 'src/utils/access';
@Injectable() @Injectable()
export class APIKeyService { export class APIKeyService extends BaseService {
constructor(
@Inject(ICryptoRepository) private crypto: ICryptoRepository,
@Inject(IKeyRepository) private repository: IKeyRepository,
) {}
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
const secret = this.crypto.newPassword(32); const secret = this.cryptoRepository.newPassword(32);
if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
throw new BadRequestException('Cannot grant permissions you do not have'); throw new BadRequestException('Cannot grant permissions you do not have');
} }
const entity = await this.repository.create({ const entity = await this.keyRepository.create({
key: this.crypto.hashSha256(secret), key: this.cryptoRepository.hashSha256(secret),
name: dto.name || 'API Key', name: dto.name || 'API Key',
userId: auth.user.id, userId: auth.user.id,
permissions: dto.permissions, permissions: dto.permissions,
@ -31,27 +25,27 @@ export class APIKeyService {
} }
async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> { async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> {
const exists = await this.repository.getById(auth.user.id, id); const exists = await this.keyRepository.getById(auth.user.id, id);
if (!exists) { if (!exists) {
throw new BadRequestException('API Key not found'); 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); return this.map(key);
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
const exists = await this.repository.getById(auth.user.id, id); const exists = await this.keyRepository.getById(auth.user.id, id);
if (!exists) { if (!exists) {
throw new BadRequestException('API Key not found'); 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<APIKeyResponseDto> { async getById(auth: AuthDto, id: string): Promise<APIKeyResponseDto> {
const key = await this.repository.getById(auth.user.id, id); const key = await this.keyRepository.getById(auth.user.id, id);
if (!key) { if (!key) {
throw new BadRequestException('API Key not found'); throw new BadRequestException('API Key not found');
} }
@ -59,7 +53,7 @@ export class APIKeyService {
} }
async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> { async getAll(auth: AuthDto): Promise<APIKeyResponseDto[]> {
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)); return keys.map((key) => this.map(key));
} }

View File

@ -6,9 +6,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType, CacheControl } from 'src/enum'; import { AssetStatus, AssetType, CacheControl } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetMediaService } from 'src/services/asset-media.service'; 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 { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub'; import { fileStub } from 'test/fixtures/file.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { QueryFailedError } from 'typeorm'; import { QueryFailedError } from 'typeorm';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
@ -189,24 +182,15 @@ const copiedAsset = Object.freeze({
describe(AssetMediaService.name, () => { describe(AssetMediaService.name, () => {
let sut: AssetMediaService; let sut: AssetMediaService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
let eventMock: Mocked<IEventRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, assetMock, jobMock, storageMock, userMock } = newTestService(AssetMediaService));
assetMock = newAssetRepositoryMock();
jobMock = newJobRepositoryMock();
loggerMock = newLoggerRepositoryMock();
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
eventMock = newEventRepositoryMock();
sut = new AssetMediaService(accessMock, assetMock, jobMock, storageMock, userMock, eventMock, loggerMock);
}); });
describe('getUploadAssetIdByChecksum', () => { describe('getUploadAssetIdByChecksum', () => {

View File

@ -1,10 +1,4 @@
import { import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { extname } from 'node:path'; import { extname } from 'node:path';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
@ -28,13 +22,8 @@ import {
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum'; import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobName } from 'src/interfaces/job.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { BaseService } from 'src/services/base.service';
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 { requireAccess, requireUploadAccess } from 'src/utils/access'; import { requireAccess, requireUploadAccess } from 'src/utils/access';
import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
@ -56,19 +45,7 @@ export interface UploadFile {
} }
@Injectable() @Injectable()
export class AssetMediaService { export class AssetMediaService extends BaseService {
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);
}
async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> { async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise<AssetMediaResponseDto | undefined> {
if (!checksum) { if (!checksum) {
return; return;
@ -148,7 +125,7 @@ export class AssetMediaService {
sidecarFile?: UploadFile, sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
try { try {
await requireAccess(this.access, { await requireAccess(this.accessRepository, {
auth, auth,
permission: Permission.ASSET_UPLOAD, permission: Permission.ASSET_UPLOAD,
// do not need an id here, but the interface requires it // do not need an id here, but the interface requires it
@ -182,7 +159,7 @@ export class AssetMediaService {
sidecarFile?: UploadFile, sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
try { 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; const asset = (await this.assetRepository.getById(id)) as AssetEntity;
this.requireQuota(auth, file.size); this.requireQuota(auth, file.size);
@ -205,7 +182,7 @@ export class AssetMediaService {
} }
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
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); const asset = await this.findOrFail(id);
if (!asset) { if (!asset) {
@ -220,7 +197,7 @@ export class AssetMediaService {
} }
async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> { async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise<ImmichFileResponse> {
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 asset = await this.findOrFail(id);
const size = dto.size ?? AssetMediaSize.THUMBNAIL; const size = dto.size ?? AssetMediaSize.THUMBNAIL;
@ -243,7 +220,7 @@ export class AssetMediaService {
} }
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
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 asset = await this.findOrFail(id);
if (!asset) { if (!asset) {

View File

@ -4,13 +4,10 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, AssetType } from 'src/enum'; import { AssetStatus, AssetType } from 'src/enum';
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetService } from 'src/services/asset.service'; import { AssetService } from 'src/services/asset.service';
import { assetStub } from 'test/fixtures/asset.stub'; 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 { faceStub } from 'test/fixtures/face.stub';
import { partnerStub } from 'test/fixtures/partner.stub'; import { partnerStub } from 'test/fixtures/partner.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
const stats: AssetStats = { const stats: AssetStats = {
@ -45,16 +34,14 @@ const statResponse: AssetStatsResponseDto = {
describe(AssetService.name, () => { describe(AssetService.name, () => {
let sut: AssetService; let sut: AssetService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>;
let userMock: Mocked<IUserRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let stackMock: Mocked<IStackRepository>; let jobMock: Mocked<IJobRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let partnerMock: Mocked<IPartnerRepository>; let partnerMock: Mocked<IPartnerRepository>;
let loggerMock: Mocked<ILoggerRepository>; let stackMock: Mocked<IStackRepository>;
let userMock: Mocked<IUserRepository>;
it('should work', () => { it('should work', () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
@ -67,29 +54,8 @@ describe(AssetService.name, () => {
}; };
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, assetMock, eventMock, jobMock, userMock, partnerMock, stackMock } =
assetMock = newAssetRepositoryMock(); newTestService(AssetService));
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,
);
mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]); mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]);
}); });

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import _ from 'lodash'; import _ from 'lodash';
import { DateTime, Duration } from 'luxon'; import { DateTime, Duration } from 'luxon';
import { import {
@ -20,46 +20,20 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, Permission } from 'src/enum'; 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 { import {
IAssetDeleteJob, IAssetDeleteJob,
IJobRepository,
ISidecarWriteJob, ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
JobItem, JobItem,
JobName, JobName,
JobStatus, JobStatus,
} from 'src/interfaces/job.interface'; } 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 { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
export class AssetService extends BaseService { 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<MemoryLaneResponseDto[]> { async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const partnerIds = await getMyPartnerIds({ const partnerIds = await getMyPartnerIds({
userId: auth.user.id, userId: auth.user.id,
@ -112,7 +86,7 @@ export class AssetService extends BaseService {
} }
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> { async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
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( const asset = await this.assetRepository.getById(
id, id,
@ -161,7 +135,7 @@ export class AssetService extends BaseService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
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 { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
const repos = { asset: this.assetRepository, event: this.eventRepository }; const repos = { asset: this.assetRepository, event: this.eventRepository };
@ -204,7 +178,7 @@ export class AssetService extends BaseService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; 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) { for (const id of ids) {
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
@ -301,7 +275,7 @@ export class AssetService extends BaseService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto; 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, { await this.assetRepository.updateAll(ids, {
deletedAt: new Date(), deletedAt: new Date(),
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED, status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
@ -310,7 +284,7 @@ export class AssetService extends BaseService {
} }
async run(auth: AuthDto, dto: AssetJobsDto) { 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[] = []; const jobs: JobItem[] = [];

View File

@ -1,46 +1,18 @@
import { DatabaseAction, EntityType } from 'src/enum'; import { DatabaseAction, EntityType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { JobStatus } from 'src/interfaces/job.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 { AuditService } from 'src/services/audit.service';
import { auditStub } from 'test/fixtures/audit.stub'; import { auditStub } from 'test/fixtures/audit.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(AuditService.name, () => { describe(AuditService.name, () => {
let sut: AuditService; let sut: AuditService;
let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>;
let auditMock: Mocked<IAuditRepository>; let auditMock: Mocked<IAuditRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>;
let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, auditMock } = newTestService(AuditService));
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);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
@ -21,44 +21,24 @@ import {
StorageFolder, StorageFolder,
UserPathType, UserPathType,
} from 'src/enum'; } 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 { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { BaseService } from 'src/services/base.service';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
export class AuditService { export class AuditService extends BaseService {
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);
}
async handleCleanup(): Promise<JobStatus> { async handleCleanup(): Promise<JobStatus> {
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; return JobStatus.SUCCESS;
} }
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
const userId = dto.userId || auth.user.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 audits = await this.repository.getAfter(dto.after, { const audits = await this.auditRepository.getAfter(dto.after, {
userIds: [userId], userIds: [userId],
entityType: dto.entityType, entityType: dto.entityType,
action: DatabaseAction.DELETE, action: DatabaseAction.DELETE,

View File

@ -5,10 +5,8 @@ import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AuthType } from 'src/enum'; import { AuthType } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.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 { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newTestService } from 'test/utils';
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 { Mock, Mocked, vitest } from 'vitest'; import { Mock, Mocked, vitest } from 'vitest';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64'); // const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -59,15 +49,14 @@ const oauthUserWithDefaultQuota = {
describe('AuthService', () => { describe('AuthService', () => {
let sut: AuthService; let sut: AuthService;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>; let cryptoMock: Mocked<ICryptoRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let sessionMock: Mocked<ISessionRepository>;
let shareMock: Mocked<ISharedLinkRepository>;
let keyMock: Mocked<IKeyRepository>; let keyMock: Mocked<IKeyRepository>;
let sessionMock: Mocked<ISessionRepository>;
let sharedLinkMock: Mocked<ISharedLinkRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let userMock: Mocked<IUserRepository>;
let callbackMock: Mock; let callbackMock: Mock;
let userinfoMock: Mock; let userinfoMock: Mock;
@ -92,27 +81,8 @@ describe('AuthService', () => {
}), }),
} as any); } as any);
configMock = newConfigRepositoryMock(); ({ sut, cryptoMock, eventMock, keyMock, sessionMock, sharedLinkMock, systemMock, userMock } =
cryptoMock = newCryptoRepositoryMock(); newTestService(AuthService));
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,
);
}); });
it('should be defined', () => { it('should be defined', () => {
@ -297,7 +267,7 @@ describe('AuthService', () => {
describe('validate - shared key', () => { describe('validate - shared key', () => {
it('should not accept a non-existent key', async () => { it('should not accept a non-existent key', async () => {
shareMock.getByKey.mockResolvedValue(null); sharedLinkMock.getByKey.mockResolvedValue(null);
await expect( await expect(
sut.authenticate({ sut.authenticate({
headers: { 'x-immich-share-key': 'key' }, headers: { 'x-immich-share-key': 'key' },
@ -308,7 +278,7 @@ describe('AuthService', () => {
}); });
it('should not accept an expired key', async () => { it('should not accept an expired key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
await expect( await expect(
sut.authenticate({ sut.authenticate({
headers: { 'x-immich-share-key': 'key' }, headers: { 'x-immich-share-key': 'key' },
@ -319,7 +289,7 @@ describe('AuthService', () => {
}); });
it('should not accept a key without a user', async () => { it('should not accept a key without a user', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.expired);
userMock.get.mockResolvedValue(null); userMock.get.mockResolvedValue(null);
await expect( await expect(
sut.authenticate({ sut.authenticate({
@ -331,7 +301,7 @@ describe('AuthService', () => {
}); });
it('should accept a base64url key', async () => { it('should accept a base64url key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
await expect( await expect(
sut.authenticate({ sut.authenticate({
@ -343,11 +313,11 @@ describe('AuthService', () => {
user: userStub.admin, user: userStub.admin,
sharedLink: sharedLinkStub.valid, sharedLink: sharedLinkStub.valid,
}); });
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
}); });
it('should accept a hex key', async () => { it('should accept a hex key', async () => {
shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.getByKey.mockResolvedValue(sharedLinkStub.valid);
userMock.get.mockResolvedValue(userStub.admin); userMock.get.mockResolvedValue(userStub.admin);
await expect( await expect(
sut.authenticate({ sut.authenticate({
@ -359,7 +329,7 @@ describe('AuthService', () => {
user: userStub.admin, user: userStub.admin,
sharedLink: sharedLinkStub.valid, sharedLink: sharedLinkStub.valid,
}); });
expect(shareMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key); expect(sharedLinkMock.getByKey).toHaveBeenCalledWith(sharedLinkStub.valid.key);
}); });
}); });

View File

@ -1,7 +1,6 @@
import { import {
BadRequestException, BadRequestException,
ForbiddenException, ForbiddenException,
Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
UnauthorizedException, UnauthorizedException,
@ -13,6 +12,7 @@ import { IncomingHttpHeaders } from 'node:http';
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
import { SystemConfig } from 'src/config'; import { SystemConfig } from 'src/config';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { OnEvent } from 'src/decorators';
import { import {
AuthDto, AuthDto,
ChangePasswordDto, ChangePasswordDto,
@ -30,15 +30,6 @@ import {
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum'; 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 { BaseService } from 'src/services/base.service';
import { isGranted } from 'src/utils/access'; import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
@ -72,20 +63,8 @@ export type ValidateRequest = {
@Injectable() @Injectable()
export class AuthService extends BaseService { export class AuthService extends BaseService {
constructor( @OnEvent({ name: 'app.bootstrap' })
@Inject(IConfigRepository) configRepository: IConfigRepository, onBootstrap() {
@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);
custom.setHttpOptionsDefaults({ timeout: 30_000 }); custom.setHttpOptionsDefaults({ timeout: 30_000 });
} }

View File

@ -1,16 +1,97 @@
import { Inject } from '@nestjs/common'; import { Inject } from '@nestjs/common';
import { SystemConfig } from 'src/config'; 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 { 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 { 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 { 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'; import { getConfig, updateConfig } from 'src/utils/config';
export class BaseService { export class BaseService {
protected storageCore: StorageCore;
constructor( constructor(
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) protected logger: ILoggerRepository, @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(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() { private get repos() {
return { return {

View File

@ -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 { IUserRepository } from 'src/interfaces/user.interface';
import { CliService } from 'src/services/cli.service'; import { CliService } from 'src/services/cli.service';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, describe, it } from 'vitest'; import { Mocked, describe, it } from 'vitest';
describe(CliService.name, () => { describe(CliService.name, () => {
let sut: CliService; let sut: CliService;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, userMock } = newTestService(CliService));
cryptoMock = newCryptoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock);
}); });
describe('resetAdminPassword', () => { describe('resetAdminPassword', () => {

View File

@ -1,26 +1,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants'; import { SALT_ROUNDS } from 'src/constants';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; 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'; import { BaseService } from 'src/services/base.service';
@Injectable() @Injectable()
export class CliService extends BaseService { 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<UserAdminResponseDto[]> { async listUsers(): Promise<UserAdminResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: true }); const users = await this.userRepository.getList({ withDeleted: true });
return users.map((user) => mapUserAdmin(user)); return users.map((user) => mapUserAdmin(user));

View File

@ -7,13 +7,13 @@ import {
} from 'src/interfaces/database.interface'; } from 'src/interfaces/database.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { DatabaseService } from 'src/services/database.service'; import { DatabaseService } from 'src/services/database.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newTestService } from 'test/utils';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(DatabaseService.name, () => { describe(DatabaseService.name, () => {
let sut: DatabaseService; let sut: DatabaseService;
let configMock: Mocked<IConfigRepository>; let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>; let databaseMock: Mocked<IDatabaseRepository>;
let loggerMock: Mocked<ILoggerRepository>; let loggerMock: Mocked<ILoggerRepository>;
@ -24,11 +24,7 @@ describe(DatabaseService.name, () => {
let versionAboveRange: string; let versionAboveRange: string;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, configMock, databaseMock, loggerMock } = newTestService(DatabaseService));
databaseMock = newDatabaseRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new DatabaseService(configMock, databaseMock, loggerMock);
extensionRange = '0.2.x'; extensionRange = '0.2.x';
databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange);

View File

@ -1,17 +1,15 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import semver from 'semver'; import semver from 'semver';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { import {
DatabaseExtension, DatabaseExtension,
DatabaseLock, DatabaseLock,
EXTENSION_NAMES, EXTENSION_NAMES,
IDatabaseRepository,
VectorExtension, VectorExtension,
VectorIndex, VectorIndex,
} from 'src/interfaces/database.interface'; } 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 CreateFailedArgs = { name: string; extension: string; otherName: string };
type UpdateFailedArgs = { name: string; extension: string; availableVersion: string }; type UpdateFailedArgs = { name: string; extension: string; availableVersion: string };
@ -63,17 +61,9 @@ const messages = {
const RETRY_DURATION = Duration.fromObject({ seconds: 5 }); const RETRY_DURATION = Duration.fromObject({ seconds: 5 });
@Injectable() @Injectable()
export class DatabaseService { export class DatabaseService extends BaseService {
private reconnection?: NodeJS.Timeout; 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 }) @OnEvent({ name: 'app.bootstrap', priority: -200 })
async onBootstrap() { async onBootstrap() {
const version = await this.databaseRepository.getPostgresVersion(); const version = await this.databaseRepository.getPostgresVersion();

View File

@ -2,15 +2,12 @@ import { BadRequestException } from '@nestjs/common';
import { DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadResponseDto } from 'src/dtos/download.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { DownloadService } from 'src/services/download.service'; import { DownloadService } from 'src/services/download.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { Readable } from 'typeorm/platform/PlatformTools.js'; import { Readable } from 'typeorm/platform/PlatformTools.js';
import { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
@ -28,7 +25,6 @@ describe(DownloadService.name, () => {
let sut: DownloadService; let sut: DownloadService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
it('should work', () => { it('should work', () => {
@ -36,12 +32,7 @@ describe(DownloadService.name, () => {
}); });
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, assetMock, storageMock } = newTestService(DownloadService));
assetMock = newAssetRepositoryMock();
loggerMock = newLoggerRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock);
}); });
describe('downloadArchive', () => { describe('downloadArchive', () => {

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { parse } from 'node:path'; import { parse } from 'node:path';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { AssetIdsDto } from 'src/dtos/asset.dto'; 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 { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { ImmichReadStream } from 'src/interfaces/storage.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { BaseService } from 'src/services/base.service';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
export class DownloadService { export class DownloadService extends BaseService {
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);
}
async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> { async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise<DownloadResponseDto> {
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
const archives: DownloadArchiveInfo[] = []; const archives: DownloadArchiveInfo[] = [];
@ -73,7 +62,7 @@ export class DownloadService {
} }
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> { async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
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 zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds); const assets = await this.assetRepository.getByIds(dto.assetIds);
@ -116,20 +105,20 @@ export class DownloadService {
if (dto.assetIds) { if (dto.assetIds) {
const assetIds = 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 }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true });
return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets }));
} }
if (dto.albumId) { if (dto.albumId) {
const albumId = 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)); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
} }
if (dto.userId) { if (dto.userId) {
const userId = 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) => return usePagination(PAGINATION_SIZE, (pagination) =>
this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), this.assetRepository.getByUserId(pagination, userId, { isVisible: true }),
); );

View File

@ -1,6 +1,4 @@
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; 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 { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISearchRepository } from 'src/interfaces/search.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 { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, beforeEach, vitest } from 'vitest'; import { Mocked, beforeEach, vitest } from 'vitest';
vitest.useFakeTimers(); vitest.useFakeTimers();
describe(SearchService.name, () => { describe(SearchService.name, () => {
let sut: DuplicateService; let sut: DuplicateService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let searchMock: Mocked<ISearchRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let searchMock: Mocked<ISearchRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, assetMock, jobMock, loggerMock, searchMock, systemMock } = newTestService(DuplicateService));
configMock = newConfigRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
searchMock = newSearchRepositoryMock();
loggerMock = newLoggerRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
sut = new DuplicateService(configMock, systemMock, searchMock, assetMock, loggerMock, cryptoMock, jobMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,22 +1,11 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto'; import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { AssetDuplicateResult } from 'src/interfaces/search.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { isDuplicateDetectionEnabled } from 'src/utils/misc';
@ -24,19 +13,6 @@ import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
export class DuplicateService extends BaseService { 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<DuplicateResponseDto[]> { async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] }); const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });

View File

@ -1,8 +1,6 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { defaults } from 'src/config'; import { defaults } from 'src/config';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { import {
IJobRepository, IJobRepository,
JobCommand, JobCommand,
@ -12,20 +10,10 @@ import {
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } 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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { JobService } from 'src/services/job.service'; import { JobService } from 'src/services/job.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
const makeMockHandlers = (status: JobStatus) => { const makeMockHandlers = (status: JobStatus) => {
@ -39,24 +27,11 @@ const makeMockHandlers = (status: JobStatus) => {
describe(JobService.name, () => { describe(JobService.name, () => {
let sut: JobService; let sut: JobService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let personMock: Mocked<IPersonRepository>;
let metricMock: Mocked<IMetricRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, assetMock, jobMock, systemMock } = newTestService(JobService));
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);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,15 +1,12 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { snakeCase } from 'lodash'; import { snakeCase } from 'lodash';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ManualJobName } from 'src/enum'; import { AssetType, ManualJobName } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { import {
ConcurrentQueueName, ConcurrentQueueName,
IJobRepository,
JobCommand, JobCommand,
JobHandler, JobHandler,
JobItem, JobItem,
@ -18,10 +15,6 @@ import {
QueueCleanType, QueueCleanType,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } 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'; import { BaseService } from 'src/services/base.service';
const asJobItem = (dto: JobCreateDto): JobItem => { const asJobItem = (dto: JobCreateDto): JobItem => {
@ -48,20 +41,6 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
export class JobService extends BaseService { export class JobService extends BaseService {
private isMicroservices = false; 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' }) @OnEvent({ name: 'app.bootstrap' })
onBootstrap(app: ArgOf<'app.bootstrap'>) { onBootstrap(app: ArgOf<'app.bootstrap'>) {
this.isMicroservices = app === 'microservices'; this.isMicroservices = app === 'microservices';

View File

@ -5,8 +5,6 @@ import { mapLibrary } from 'src/dtos/library.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.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 { IDatabaseRepository } from 'src/interfaces/database.interface';
import { import {
IJobRepository, IJobRepository,
@ -17,7 +15,6 @@ import {
JobStatus, JobStatus,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { LibraryService } from 'src/services/library.service'; 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 { libraryStub } from 'test/fixtures/library.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { makeMockWatcher } from 'test/repositories/storage.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, vitest } from 'vitest'; import { Mocked, vitest } from 'vitest';
async function* mockWalk() { async function* mockWalk() {
@ -45,37 +35,14 @@ describe(LibraryService.name, () => {
let sut: LibraryService; let sut: LibraryService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>; let databaseMock: Mocked<IDatabaseRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let libraryMock: Mocked<ILibraryRepository>; let libraryMock: Mocked<ILibraryRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, assetMock, databaseMock, jobMock, libraryMock, storageMock, systemMock } = newTestService(LibraryService));
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,
);
databaseMock.tryLock.mockResolvedValue(true); databaseMock.tryLock.mockResolvedValue(true);
}); });

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { R_OK } from 'node:constants'; import { R_OK } from 'node:constants';
import path, { basename, parse } from 'node:path'; import path, { basename, parse } from 'node:path';
import picomatch from 'picomatch'; import picomatch from 'picomatch';
@ -17,24 +17,16 @@ import {
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { LibraryEntity } from 'src/entities/library.entity'; import { LibraryEntity } from 'src/entities/library.entity';
import { AssetType } from 'src/enum'; import { AssetType } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { DatabaseLock } from 'src/interfaces/database.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 } from 'src/interfaces/event.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { import {
IEntityJob, IEntityJob,
IJobRepository,
ILibraryAssetJob, ILibraryAssetJob,
ILibraryFileJob, ILibraryFileJob,
JobName, JobName,
JOBS_LIBRARY_PAGINATION_SIZE, JOBS_LIBRARY_PAGINATION_SIZE,
JobStatus, JobStatus,
} from 'src/interfaces/job.interface'; } 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 { BaseService } from 'src/services/base.service';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { handlePromiseError } from 'src/utils/misc'; import { handlePromiseError } from 'src/utils/misc';
@ -47,21 +39,6 @@ export class LibraryService extends BaseService {
private watchLock = false; private watchLock = false;
private watchers: Record<string, () => Promise<void>> = {}; private watchers: Record<string, () => Promise<void>> = {};
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' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap() { async onBootstrap() {
const config = await this.getConfig({ withCache: false }); const config = await this.getConfig({ withCache: false });
@ -217,14 +194,14 @@ export class LibraryService extends BaseService {
return false; return false;
} }
const libraries = await this.repository.getAll(false); const libraries = await this.libraryRepository.getAll(false);
for (const library of libraries) { for (const library of libraries) {
await this.watch(library.id); await this.watch(library.id);
} }
} }
async getStatistics(id: string): Promise<LibraryStatsResponseDto> { async getStatistics(id: string): Promise<LibraryStatsResponseDto> {
const statistics = await this.repository.getStatistics(id); const statistics = await this.libraryRepository.getStatistics(id);
if (!statistics) { if (!statistics) {
throw new BadRequestException(`Library ${id} not found`); throw new BadRequestException(`Library ${id} not found`);
} }
@ -237,13 +214,13 @@ export class LibraryService extends BaseService {
} }
async getAll(): Promise<LibraryResponseDto[]> { async getAll(): Promise<LibraryResponseDto[]> {
const libraries = await this.repository.getAll(false); const libraries = await this.libraryRepository.getAll(false);
return libraries.map((library) => mapLibrary(library)); return libraries.map((library) => mapLibrary(library));
} }
async handleQueueCleanup(): Promise<JobStatus> { async handleQueueCleanup(): Promise<JobStatus> {
this.logger.debug('Cleaning up any pending library deletions'); 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( await this.jobRepository.queueAll(
pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), 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<LibraryResponseDto> { async create(dto: CreateLibraryDto): Promise<LibraryResponseDto> {
const library = await this.repository.create({ const library = await this.libraryRepository.create({
ownerId: dto.ownerId, ownerId: dto.ownerId,
name: dto.name ?? 'New External Library', name: dto.name ?? 'New External Library',
importPaths: dto.importPaths ?? [], importPaths: dto.importPaths ?? [],
@ -326,7 +303,7 @@ export class LibraryService extends BaseService {
async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> { async update(id: string, dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
await this.findOrFail(id); await this.findOrFail(id);
const library = await this.repository.update({ id, ...dto }); const library = await this.libraryRepository.update({ id, ...dto });
if (dto.importPaths) { if (dto.importPaths) {
const validation = await this.validate(id, { importPaths: 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.unwatch(id);
} }
await this.repository.softDelete(id); await this.libraryRepository.softDelete(id);
await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } }); await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
} }
@ -379,7 +356,7 @@ export class LibraryService extends BaseService {
if (!assetsFound) { if (!assetsFound) {
this.logger.log(`Deleting library ${libraryId}`); this.logger.log(`Deleting library ${libraryId}`);
await this.repository.delete(libraryId); await this.libraryRepository.delete(libraryId);
} }
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
@ -407,7 +384,7 @@ export class LibraryService extends BaseService {
this.logger.log(`Importing new library asset: ${assetPath}`); 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) { if (!library || library.deletedAt) {
this.logger.error('Cannot import asset into deleted library'); this.logger.error('Cannot import asset into deleted library');
return JobStatus.FAILED; return JobStatus.FAILED;
@ -477,7 +454,7 @@ export class LibraryService extends BaseService {
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); 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( await this.jobRepository.queueAll(
libraries.map((library) => ({ libraries.map((library) => ({
name: JobName.LIBRARY_QUEUE_SYNC_FILES, name: JobName.LIBRARY_QUEUE_SYNC_FILES,
@ -553,7 +530,7 @@ export class LibraryService extends BaseService {
} }
async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> { async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> {
const library = await this.repository.get(job.id); const library = await this.libraryRepository.get(job.id);
if (!library) { if (!library) {
this.logger.debug(`Library ${job.id} not found, skipping refresh`); this.logger.debug(`Library ${job.id} not found, skipping refresh`);
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
@ -598,13 +575,13 @@ export class LibraryService extends BaseService {
this.logger.warn(`No valid import paths found for library ${library.id}`); 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; return JobStatus.SUCCESS;
} }
async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> { async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> {
const library = await this.repository.get(job.id); const library = await this.libraryRepository.get(job.id);
if (!library) { if (!library) {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
@ -636,7 +613,7 @@ export class LibraryService extends BaseService {
} }
private async findOrFail(id: string) { private async findOrFail(id: string) {
const library = await this.repository.get(id); const library = await this.libraryRepository.get(id);
if (!library) { if (!library) {
throw new BadRequestException('Library not found'); throw new BadRequestException('Library not found');
} }

View File

@ -1,26 +1,19 @@
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IMapRepository } from 'src/interfaces/map.interface'; import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { MapService } from 'src/services/map.service'; import { MapService } from 'src/services/map.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(MapService.name, () => { describe(MapService.name, () => {
let sut: MapService; let sut: MapService;
let albumMock: Mocked<IAlbumRepository>;
let partnerMock: Mocked<IPartnerRepository>;
let mapMock: Mocked<IMapRepository>; let mapMock: Mocked<IMapRepository>;
let partnerMock: Mocked<IPartnerRepository>;
beforeEach(() => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); ({ sut, mapMock, partnerMock } = newTestService(MapService));
partnerMock = newPartnerRepositoryMock();
mapMock = newMapRepositoryMock();
sut = new MapService(albumMock, partnerMock, mapMock);
}); });
describe('getMapMarkers', () => { describe('getMapMarkers', () => {

View File

@ -1,18 +1,9 @@
import { Inject } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto'; import { MapMarkerDto, MapMarkerResponseDto, MapReverseGeocodeDto } from 'src/dtos/map.dto';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { BaseService } from 'src/services/base.service';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
export class MapService { export class MapService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IMapRepository) private mapRepository: IMapRepository,
) {}
async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> { async getMapMarkers(auth: AuthDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
const userIds = [auth.user.id]; const userIds = [auth.user.id];
if (options.withPartners) { if (options.withPartners) {

View File

@ -12,12 +12,9 @@ import {
VideoCodec, VideoCodec,
} from 'src/enum'; } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; 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 { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface'; import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.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 { faceStub } from 'test/fixtures/face.stub';
import { probeStub } from 'test/fixtures/media.stub'; import { probeStub } from 'test/fixtures/media.stub';
import { personStub } from 'test/fixtures/person.stub'; import { personStub } from 'test/fixtures/person.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(MediaService.name, () => { describe(MediaService.name, () => {
let sut: MediaService; let sut: MediaService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let mediaMock: Mocked<IMediaRepository>; let mediaMock: Mocked<IMediaRepository>;
let moveMock: Mocked<IMoveRepository>;
let personMock: Mocked<IPersonRepository>; let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, assetMock, jobMock, loggerMock, mediaMock, personMock, storageMock, systemMock } =
configMock = newConfigRepositoryMock(); newTestService(MediaService));
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,
);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { dirname } from 'node:path'; import { dirname } from 'node:path';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
@ -17,31 +17,17 @@ import {
VideoCodec, VideoCodec,
VideoContainer, VideoContainer,
} from 'src/enum'; } from 'src/enum';
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface'; import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { import {
IBaseJob, IBaseJob,
IEntityJob, IEntityJob,
IJobRepository,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
JobItem, JobItem,
JobName, JobName,
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AudioStreamInfo, TranscodeCommand, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
@ -50,36 +36,9 @@ import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
export class MediaService extends BaseService { export class MediaService extends BaseService {
private storageCore: StorageCore;
private maliOpenCL?: boolean; private maliOpenCL?: boolean;
private devices?: string[]; 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<JobStatus> { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force

View File

@ -5,20 +5,18 @@ import { MemoryService } from 'src/services/memory.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { memoryStub } from 'test/fixtures/memory.stub'; import { memoryStub } from 'test/fixtures/memory.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; import { newTestService } from 'test/utils';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(MemoryService.name, () => { describe(MemoryService.name, () => {
let accessMock: IAccessRepositoryMock;
let memoryMock: Mocked<IMemoryRepository>;
let sut: MemoryService; let sut: MemoryService;
beforeEach(() => { let accessMock: IAccessRepositoryMock;
accessMock = newAccessRepositoryMock(); let memoryMock: Mocked<IMemoryRepository>;
memoryMock = newMemoryRepositoryMock();
sut = new MemoryService(accessMock, memoryMock); beforeEach(() => {
({ sut, accessMock, memoryMock } = newTestService(MemoryService));
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -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 { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { BaseService } from 'src/services/base.service';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { checkAccess, requireAccess } from 'src/utils/access'; import { checkAccess, requireAccess } from 'src/utils/access';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
@Injectable() @Injectable()
export class MemoryService { export class MemoryService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IMemoryRepository) private repository: IMemoryRepository,
) {}
async search(auth: AuthDto) { 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)); return memories.map((memory) => mapMemory(memory));
} }
async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> { async get(auth: AuthDto, id: string): Promise<MemoryResponseDto> {
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); const memory = await this.findOrFail(id);
return mapMemory(memory); return mapMemory(memory);
} }
@ -31,12 +25,12 @@ export class MemoryService {
// TODO validate type/data combination // TODO validate type/data combination
const assetIds = dto.assetIds || []; const assetIds = dto.assetIds || [];
const allowedAssetIds = await checkAccess(this.access, { const allowedAssetIds = await checkAccess(this.accessRepository, {
auth, auth,
permission: Permission.ASSET_SHARE, permission: Permission.ASSET_SHARE,
ids: assetIds, ids: assetIds,
}); });
const memory = await this.repository.create({ const memory = await this.memoryRepository.create({
ownerId: auth.user.id, ownerId: auth.user.id,
type: dto.type, type: dto.type,
data: dto.data, data: dto.data,
@ -50,9 +44,9 @@ export class MemoryService {
} }
async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> { async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise<MemoryResponseDto> {
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, id,
isSaved: dto.isSaved, isSaved: dto.isSaved,
memoryAt: dto.memoryAt, memoryAt: dto.memoryAt,
@ -63,28 +57,28 @@ export class MemoryService {
} }
async remove(auth: AuthDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.MEMORY_DELETE, ids: [id] });
await this.repository.delete(id); await this.memoryRepository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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 results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.repository.update({ id, updatedAt: new Date() }); await this.memoryRepository.update({ id, updatedAt: new Date() });
} }
return results; return results;
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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, { const results = await removeAssets(auth, repos, {
parentId: id, parentId: id,
assetIds: dto.ids, assetIds: dto.ids,
@ -93,14 +87,14 @@ export class MemoryService {
const hasSuccess = results.find(({ success }) => success); const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) { if (hasSuccess) {
await this.repository.update({ id, updatedAt: new Date() }); await this.memoryRepository.update({ id, updatedAt: new Date() });
} }
return results; return results;
} }
private async findOrFail(id: string) { private async findOrFail(id: string) {
const memory = await this.repository.get(id); const memory = await this.memoryRepository.get(id);
if (!memory) { if (!memory) {
throw new BadRequestException('Memory not found'); throw new BadRequestException('Memory not found');
} }

View File

@ -6,16 +6,12 @@ import { ExifEntity } from 'src/entities/exif.entity';
import { AssetType, SourceType } from 'src/enum'; import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { IMapRepository } from 'src/interfaces/map.interface';
import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.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 { metadataStub } from 'test/fixtures/metadata.stub';
import { personStub } from 'test/fixtures/person.stub'; import { personStub } from 'test/fixtures/person.stub';
import { tagStub } from 'test/fixtures/tag.stub'; import { tagStub } from 'test/fixtures/tag.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(MetadataService.name, () => { describe(MetadataService.name, () => {
@ -52,60 +32,35 @@ describe(MetadataService.name, () => {
let albumMock: Mocked<IAlbumRepository>; let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>; let cryptoMock: Mocked<ICryptoRepository>;
let cryptoRepository: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let mapMock: Mocked<IMapRepository>; let mapMock: Mocked<IMapRepository>;
let metadataMock: Mocked<IMetadataRepository>;
let moveMock: Mocked<IMoveRepository>;
let mediaMock: Mocked<IMediaRepository>; let mediaMock: Mocked<IMediaRepository>;
let metadataMock: Mocked<IMetadataRepository>;
let personMock: Mocked<IPersonRepository>; let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let tagMock: Mocked<ITagRepository>; let tagMock: Mocked<ITagRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); ({
assetMock = newAssetRepositoryMock(); sut,
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(
albumMock, albumMock,
assetMock, assetMock,
configMock, cryptoMock,
cryptoRepository,
databaseMock,
eventMock, eventMock,
jobMock, jobMock,
mapMock, mapMock,
mediaMock, mediaMock,
metadataMock, metadataMock,
moveMock,
personMock, personMock,
storageMock, storageMock,
systemMock, systemMock,
tagMock, tagMock,
userMock, userMock,
loggerMock, } = newTestService(MetadataService));
);
}); });
afterEach(async () => { afterEach(async () => {
@ -569,10 +524,10 @@ describe(MetadataService.name, () => {
EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoFile: new BinaryField(0, ''),
EmbeddedVideoType: 'MotionPhoto_Data', EmbeddedVideoType: 'MotionPhoto_Data',
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null); assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512); const video = randomBytes(512);
metadataMock.extractBinaryTag.mockResolvedValue(video); metadataMock.extractBinaryTag.mockResolvedValue(video);
@ -612,10 +567,10 @@ describe(MetadataService.name, () => {
EmbeddedVideoFile: new BinaryField(0, ''), EmbeddedVideoFile: new BinaryField(0, ''),
EmbeddedVideoType: 'MotionPhoto_Data', EmbeddedVideoType: 'MotionPhoto_Data',
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null); assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512); const video = randomBytes(512);
metadataMock.extractBinaryTag.mockResolvedValue(video); metadataMock.extractBinaryTag.mockResolvedValue(video);
@ -656,10 +611,10 @@ describe(MetadataService.name, () => {
MicroVideo: 1, MicroVideo: 1,
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null); assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512); const video = randomBytes(512);
storageMock.readFile.mockResolvedValue(video); storageMock.readFile.mockResolvedValue(video);
@ -700,7 +655,7 @@ describe(MetadataService.name, () => {
MicroVideo: 1, MicroVideo: 1,
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null); assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset })); assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }));
const video = randomBytes(512); const video = randomBytes(512);
@ -725,7 +680,7 @@ describe(MetadataService.name, () => {
MicroVideo: 1, MicroVideo: 1,
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset); assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
const video = randomBytes(512); const video = randomBytes(512);
storageMock.readFile.mockResolvedValue(video); storageMock.readFile.mockResolvedValue(video);
@ -747,7 +702,7 @@ describe(MetadataService.name, () => {
MicroVideo: 1, MicroVideo: 1,
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true }); assetMock.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
const video = randomBytes(512); const video = randomBytes(512);
storageMock.readFile.mockResolvedValue(video); storageMock.readFile.mockResolvedValue(video);
@ -773,7 +728,7 @@ describe(MetadataService.name, () => {
MicroVideo: 1, MicroVideo: 1,
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null); assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
const video = randomBytes(512); const video = randomBytes(512);

View File

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored'; import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import _ from 'lodash'; import _ from 'lodash';
@ -13,32 +13,20 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity'; import { ExifEntity } from 'src/entities/exif.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, SourceType } from 'src/enum'; import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { WithoutProperty } from 'src/interfaces/asset.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { DatabaseLock } from 'src/interfaces/database.interface';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { ArgOf } from 'src/interfaces/event.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 { import {
IBaseJob, IBaseJob,
IEntityJob, IEntityJob,
IJobRepository,
ISidecarWriteJob, ISidecarWriteJob,
JobName, JobName,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ReverseGeocodeResult } from 'src/interfaces/map.interface';
import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface'; import { ImmichTags } from 'src/interfaces/metadata.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { isFaceImportEnabled } from 'src/utils/misc'; import { isFaceImportEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@ -99,41 +87,6 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
@Injectable() @Injectable()
export class MetadataService extends BaseService { 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' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap(app: ArgOf<'app.bootstrap'>) { async onBootstrap(app: ArgOf<'app.bootstrap'>) {
if (app !== 'microservices') { if (app !== 'microservices') {
@ -145,7 +98,7 @@ export class MetadataService extends BaseService {
@OnEvent({ name: 'app.shutdown' }) @OnEvent({ name: 'app.shutdown' })
async onShutdown() { async onShutdown() {
await this.repository.teardown(); await this.metadataRepository.teardown();
} }
@OnEvent({ name: 'config.update' }) @OnEvent({ name: 'config.update' })
@ -372,7 +325,7 @@ export class MetadataService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
await this.repository.writeTags(sidecarPath, exif); await this.metadataRepository.writeTags(sidecarPath, exif);
if (!asset.sidecarPath) { if (!asset.sidecarPath) {
await this.assetRepository.update({ id, sidecarPath }); await this.assetRepository.update({ id, sidecarPath });
@ -382,8 +335,8 @@ export class MetadataService extends BaseService {
} }
private async getExifTags(asset: AssetEntity): Promise<ImmichTags> { private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
const mediaTags = await this.repository.readTags(asset.originalPath); const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {}; const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};
const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {}; const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {};
// make sure dates comes from sidecar // make sure dates comes from sidecar
@ -467,11 +420,11 @@ export class MetadataService extends BaseService {
// Samsung MotionPhoto video extraction // Samsung MotionPhoto video extraction
// HEIC-encoded // HEIC-encoded
if (hasMotionPhotoVideo) { 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 // JPEG-encoded; HEIC also contains these tags, so this conditional must come second
else if (hasEmbeddedVideoFile) { else if (hasEmbeddedVideoFile) {
video = await this.repository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile'); video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
} }
// Default video extraction // Default video extraction
else { else {

View File

@ -6,10 +6,8 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity';
import { AssetFileType, UserMetadataKey } from 'src/enum'; import { AssetFileType, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.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 { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const configs = { const configs = {
@ -66,39 +56,19 @@ const configs = {
}; };
describe(NotificationService.name, () => { describe(NotificationService.name, () => {
let sut: NotificationService;
let albumMock: Mocked<IAlbumRepository>; let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let notificationMock: Mocked<INotificationRepository>; let notificationMock: Mocked<INotificationRepository>;
let sut: NotificationService;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
beforeEach(() => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); ({ sut, albumMock, assetMock, eventMock, jobMock, notificationMock, systemMock, userMock } =
assetMock = newAssetRepositoryMock(); newTestService(NotificationService));
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,
);
}); });
it('should work', () => { it('should work', () => {

View File

@ -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 { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumEntity } from 'src/entities/album.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { import {
IEmailJob, IEmailJob,
IJobRepository,
INotifyAlbumInviteJob, INotifyAlbumInviteJob,
INotifyAlbumUpdateJob, INotifyAlbumUpdateJob,
INotifySignupJob, INotifySignupJob,
JobName, JobName,
JobStatus, JobStatus,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { getFilenameExtension } from 'src/utils/file'; import { getFilenameExtension } from 'src/utils/file';
@ -28,21 +21,6 @@ import { getPreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
export class NotificationService extends BaseService { 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' }) @OnEvent({ name: 'config.update' })
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
this.eventRepository.clientBroadcast('on_config_update'); this.eventRepository.clientBroadcast('on_config_update');

View File

@ -1,20 +1,17 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface'; import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.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'; import { Mocked } from 'vitest';
describe(PartnerService.name, () => { describe(PartnerService.name, () => {
let sut: PartnerService; let sut: PartnerService;
let partnerMock: Mocked<IPartnerRepository>; let partnerMock: Mocked<IPartnerRepository>;
let accessMock: Mocked<IAccessRepository>;
beforeEach(() => { beforeEach(() => {
partnerMock = newPartnerRepositoryMock(); ({ sut, partnerMock } = newTestService(PartnerService));
sut = new PartnerService(partnerMock, accessMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -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 { AuthDto } from 'src/dtos/auth.dto';
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto'; import { mapUser } from 'src/dtos/user.dto';
import { PartnerEntity } from 'src/entities/partner.entity'; import { PartnerEntity } from 'src/entities/partner.entity';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class PartnerService { export class PartnerService extends BaseService {
constructor(
@Inject(IPartnerRepository) private repository: IPartnerRepository,
@Inject(IAccessRepository) private access: IAccessRepository,
) {}
async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> { async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const exists = await this.repository.get(partnerId); const exists = await this.partnerRepository.get(partnerId);
if (exists) { if (exists) {
throw new BadRequestException(`Partner already 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); return this.mapPartner(partner, PartnerDirection.SharedBy);
} }
async remove(auth: AuthDto, sharedWithId: string): Promise<void> { async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const partner = await this.repository.get(partnerId); const partner = await this.partnerRepository.get(partnerId);
if (!partner) { if (!partner) {
throw new BadRequestException('Partner not found'); throw new BadRequestException('Partner not found');
} }
await this.repository.remove(partner); await this.partnerRepository.remove(partner);
} }
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> { async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
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'; const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
return partners return partners
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users .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<PartnerResponseDto> { async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
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 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); return this.mapPartner(entity, PartnerDirection.SharedWith);
} }

View File

@ -4,13 +4,10 @@ import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum'; import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { DetectedFaces, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMediaRepository } from 'src/interfaces/media.interface'; import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface'; import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
import { IStorageRepository } from 'src/interfaces/storage.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 { faceStub } from 'test/fixtures/face.stub';
import { personStub } from 'test/fixtures/person.stub'; import { personStub } from 'test/fixtures/person.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
@ -67,51 +53,33 @@ const detectFaceMock: DetectedFaces = {
}; };
describe(PersonService.name, () => { describe(PersonService.name, () => {
let sut: PersonService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>; let cryptoMock: Mocked<ICryptoRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let machineLearningMock: Mocked<IMachineLearningRepository>; let machineLearningMock: Mocked<IMachineLearningRepository>;
let mediaMock: Mocked<IMediaRepository>; let mediaMock: Mocked<IMediaRepository>;
let moveMock: Mocked<IMoveRepository>;
let personMock: Mocked<IPersonRepository>; let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>;
let searchMock: Mocked<ISearchRepository>; let searchMock: Mocked<ISearchRepository>;
let cryptoMock: Mocked<ICryptoRepository>; let storageMock: Mocked<IStorageRepository>;
let loggerMock: Mocked<ILoggerRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let sut: PersonService;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({
assetMock = newAssetRepositoryMock(); sut,
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(
accessMock, accessMock,
assetMock, assetMock,
configMock, cryptoMock,
jobMock,
machineLearningMock, machineLearningMock,
moveMock,
mediaMock, mediaMock,
personMock, personMock,
systemMock,
storageMock,
jobMock,
searchMock, searchMock,
cryptoMock, storageMock,
loggerMock, systemMock,
); } = newTestService(PersonService));
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -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 { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
@ -31,15 +31,11 @@ import {
SourceType, SourceType,
SystemMetadataKey, SystemMetadataKey,
} from 'src/enum'; } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { WithoutProperty } from 'src/interfaces/asset.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { import {
IBaseJob, IBaseJob,
IDeferrableJob, IDeferrableJob,
IEntityJob, IEntityJob,
IJobRepository,
INightlyJob, INightlyJob,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
JobItem, JobItem,
@ -47,14 +43,9 @@ import {
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { BoundingBox } from 'src/interfaces/machine-learning.interface';
import { BoundingBox, IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { CropOptions, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface';
import { CropOptions, IMediaRepository, ImageDimensions, InputDimensions } from 'src/interfaces/media.interface'; import { UpdateFacesData } from 'src/interfaces/person.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { checkAccess, requireAccess } from 'src/utils/access'; import { checkAccess, requireAccess } from 'src/utils/access';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
@ -66,37 +57,6 @@ import { IsNull } from 'typeorm';
@Injectable() @Injectable()
export class PersonService extends BaseService { 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<PeopleResponseDto> { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
const { withHidden = false, page, size } = dto; const { withHidden = false, page, size } = dto;
const pagination = { const pagination = {
@ -105,11 +65,11 @@ export class PersonService extends BaseService {
}; };
const { machineLearning } = await this.getConfig({ withCache: false }); 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, minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden, withHidden,
}); });
const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id); const { total, hidden } = await this.personRepository.getNumberOfPeople(auth.user.id);
return { return {
people: items.map((person) => mapPerson(person)), people: items.map((person) => mapPerson(person)),
@ -120,15 +80,15 @@ export class PersonService extends BaseService {
} }
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> { async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
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 person = await this.findOrFail(personId);
const result: PersonResponseDto[] = []; const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = []; const changeFeaturePhoto: string[] = [];
for (const data of dto.data) { 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) { 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) { if (person.faceAssetId === null) {
changeFeaturePhoto.push(person.id); changeFeaturePhoto.push(person.id);
} }
@ -136,7 +96,7 @@ export class PersonService extends BaseService {
changeFeaturePhoto.push(face.person.id); changeFeaturePhoto.push(face.person.id);
} }
await this.repository.reassignFace(face.id, personId); await this.personRepository.reassignFace(face.id, personId);
} }
result.push(person); result.push(person);
@ -149,12 +109,12 @@ export class PersonService extends BaseService {
} }
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> { async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] });
await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] });
const face = await this.repository.getFaceById(dto.id); const face = await this.personRepository.getFaceById(dto.id);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
await this.repository.reassignFace(face.id, personId); await this.personRepository.reassignFace(face.id, personId);
if (person.faceAssetId === null) { if (person.faceAssetId === null) {
await this.createNewFeaturePhoto([person.id]); await this.createNewFeaturePhoto([person.id]);
} }
@ -166,8 +126,8 @@ export class PersonService extends BaseService {
} }
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> { async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [dto.id] });
const faces = await this.repository.getFaces(dto.id); const faces = await this.personRepository.getFaces(dto.id);
return faces.map((asset) => mapFaces(asset, auth)); return faces.map((asset) => mapFaces(asset, auth));
} }
@ -178,10 +138,10 @@ export class PersonService extends BaseService {
const jobs: JobItem[] = []; const jobs: JobItem[] = [];
for (const personId of changeFeaturePhoto) { for (const personId of changeFeaturePhoto) {
const assetFace = await this.repository.getRandomFace(personId); const assetFace = await this.personRepository.getRandomFace(personId);
if (assetFace !== null) { 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 } }); 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<PersonResponseDto> { async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
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); return this.findOrFail(id).then(mapPerson);
} }
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> { async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
return this.repository.getStatistics(id); return this.personRepository.getStatistics(id);
} }
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
const person = await this.repository.getById(id); const person = await this.personRepository.getById(id);
if (!person || !person.thumbnailPath) { if (!person || !person.thumbnailPath) {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -214,13 +174,13 @@ export class PersonService extends BaseService {
} }
async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> { async getAssets(auth: AuthDto, id: string): Promise<AssetResponseDto[]> {
await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); await requireAccess(this.accessRepository, { auth, permission: Permission.PERSON_READ, ids: [id] });
const assets = await this.repository.getAssets(id); const assets = await this.personRepository.getAssets(id);
return assets.map((asset) => mapAsset(asset)); return assets.map((asset) => mapAsset(asset));
} }
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> { create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.repository.create({ return this.personRepository.create({
ownerId: auth.user.id, ownerId: auth.user.id,
name: dto.name, name: dto.name,
birthDate: dto.birthDate, birthDate: dto.birthDate,
@ -229,14 +189,14 @@ export class PersonService extends BaseService {
} }
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
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; const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
// TODO: set by faceId directly // TODO: set by faceId directly
let faceId: string | undefined = undefined; let faceId: string | undefined = undefined;
if (assetId) { if (assetId) {
await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_READ, ids: [assetId] });
const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]);
if (!face) { if (!face) {
throw new BadRequestException('Invalid assetId for feature face'); throw new BadRequestException('Invalid assetId for feature face');
} }
@ -244,7 +204,7 @@ export class PersonService extends BaseService {
faceId = face.id; 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) { if (assetId) {
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); 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[]) { private async delete(people: PersonEntity[]) {
await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath))); 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`); this.logger.debug(`Deleted ${people.length} people`);
} }
async handlePersonCleanup(): Promise<JobStatus> { async handlePersonCleanup(): Promise<JobStatus> {
const people = await this.repository.getAllWithoutFaces(); const people = await this.personRepository.getAllWithoutFaces();
await this.delete(people); await this.delete(people);
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
@ -291,7 +251,7 @@ export class PersonService extends BaseService {
} }
if (force) { if (force) {
await this.repository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup(); 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 } }))); await this.jobRepository.queueAll(faceIds.map((id) => ({ name: JobName.FACIAL_RECOGNITION, data: { id } })));
} }
@ -387,7 +347,7 @@ export class PersonService extends BaseService {
if (nightly) { if (nightly) {
const [state, latestFaceDate] = await Promise.all([ const [state, latestFaceDate] = await Promise.all([
this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE), this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE),
this.repository.getLatestFaceDate(), this.personRepository.getLatestFaceDate(),
]); ]);
if (state?.lastRun && latestFaceDate && state.lastRun > latestFaceDate) { 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); const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
if (force) { if (force) {
await this.repository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
await this.handlePersonCleanup(); await this.handlePersonCleanup();
} else if (waiting) { } else if (waiting) {
this.logger.debug( this.logger.debug(
@ -410,7 +370,7 @@ export class PersonService extends BaseService {
const lastRun = new Date().toISOString(); const lastRun = new Date().toISOString();
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.repository.getAllFaces(pagination, { this.personRepository.getAllFaces(pagination, {
where: force ? undefined : { personId: IsNull(), sourceType: IsNull() }, where: force ? undefined : { personId: IsNull(), sourceType: IsNull() },
}), }),
); );
@ -432,7 +392,7 @@ export class PersonService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const face = await this.repository.getFaceByIdWithAssets( const face = await this.personRepository.getFaceByIdWithAssets(
id, id,
{ person: true, asset: true, faceSearch: true }, { person: true, asset: true, faceSearch: true },
{ id: true, personId: true, sourceType: true, faceSearch: { embedding: true } }, { id: true, personId: true, sourceType: true, faceSearch: { embedding: true } },
@ -457,7 +417,7 @@ export class PersonService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const matches = await this.smartInfoRepository.searchFaces({ const matches = await this.searchRepository.searchFaces({
userIds: [face.asset.ownerId], userIds: [face.asset.ownerId],
embedding: face.faceSearch.embedding, embedding: face.faceSearch.embedding,
maxDistance: machineLearning.facialRecognition.maxDistance, maxDistance: machineLearning.facialRecognition.maxDistance,
@ -481,7 +441,7 @@ export class PersonService extends BaseService {
let personId = matches.find((match) => match.face.personId)?.face.personId; let personId = matches.find((match) => match.face.personId)?.face.personId;
if (!personId) { if (!personId) {
const matchWithPerson = await this.smartInfoRepository.searchFaces({ const matchWithPerson = await this.searchRepository.searchFaces({
userIds: [face.asset.ownerId], userIds: [face.asset.ownerId],
embedding: face.faceSearch.embedding, embedding: face.faceSearch.embedding,
maxDistance: machineLearning.facialRecognition.maxDistance, maxDistance: machineLearning.facialRecognition.maxDistance,
@ -496,21 +456,21 @@ export class PersonService extends BaseService {
if (isCore && !personId) { if (isCore && !personId) {
this.logger.log(`Creating new person for face ${id}`); 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 } }); await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
personId = newPerson.id; personId = newPerson.id;
} }
if (personId) { if (personId) {
this.logger.debug(`Assigning face ${id} to person ${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; return JobStatus.SUCCESS;
} }
async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> { async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> {
const person = await this.repository.getById(id); const person = await this.personRepository.getById(id);
if (!person) { if (!person) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
@ -526,13 +486,13 @@ export class PersonService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
const person = await this.repository.getById(data.id); const person = await this.personRepository.getById(data.id);
if (!person?.faceAssetId) { if (!person?.faceAssetId) {
this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`); this.logger.error(`Could not generate person thumbnail: person ${person?.id} has no face asset`);
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId); const face = await this.personRepository.getFaceByIdWithAssets(person.faceAssetId);
if (face === null) { if (face === null) {
this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`); this.logger.error(`Could not generate person thumbnail: face ${person.faceAssetId} not found`);
return JobStatus.FAILED; return JobStatus.FAILED;
@ -572,7 +532,7 @@ export class PersonService extends BaseService {
}; };
await this.mediaRepository.generateThumbnail(inputPath, thumbnailOptions, thumbnailPath); 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; return JobStatus.SUCCESS;
} }
@ -583,13 +543,13 @@ export class PersonService extends BaseService {
throw new BadRequestException('Cannot merge a person into themselves'); 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); let primaryPerson = await this.findOrFail(id);
const primaryName = primaryPerson.name || primaryPerson.id; const primaryName = primaryPerson.name || primaryPerson.id;
const results: BulkIdResponseDto[] = []; const results: BulkIdResponseDto[] = [];
const allowedIds = await checkAccess(this.access, { const allowedIds = await checkAccess(this.accessRepository, {
auth, auth,
permission: Permission.PERSON_MERGE, permission: Permission.PERSON_MERGE,
ids: mergeIds, ids: mergeIds,
@ -603,7 +563,7 @@ export class PersonService extends BaseService {
} }
try { try {
const mergePerson = await this.repository.getById(mergeId); const mergePerson = await this.personRepository.getById(mergeId);
if (!mergePerson) { if (!mergePerson) {
results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND }); results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND });
continue; continue;
@ -619,14 +579,14 @@ export class PersonService extends BaseService {
} }
if (Object.keys(update).length > 0) { 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 mergeName = mergePerson.name || mergePerson.id;
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id }; const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
this.logger.log(`Merging ${mergeName} into ${primaryName}`); this.logger.log(`Merging ${mergeName} into ${primaryName}`);
await this.repository.reassignFaces(mergeData); await this.personRepository.reassignFaces(mergeData);
await this.delete([mergePerson]); await this.delete([mergePerson]);
this.logger.log(`Merged ${mergeName} into ${primaryName}`); this.logger.log(`Merged ${mergeName} into ${primaryName}`);
@ -640,7 +600,7 @@ export class PersonService extends BaseService {
} }
private async findOrFail(id: string) { private async findOrFail(id: string) {
const person = await this.repository.getById(id); const person = await this.personRepository.getById(id);
if (!person) { if (!person) {
throw new BadRequestException('Person not found'); throw new BadRequestException('Person not found');
} }

View File

@ -1,60 +1,26 @@
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto';
import { IAssetRepository } from 'src/interfaces/asset.interface'; 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 { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { personStub } from 'test/fixtures/person.stub'; import { personStub } from 'test/fixtures/person.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, beforeEach, vitest } from 'vitest'; import { Mocked, beforeEach, vitest } from 'vitest';
vitest.useFakeTimers(); vitest.useFakeTimers();
describe(SearchService.name, () => { describe(SearchService.name, () => {
let sut: SearchService; let sut: SearchService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let machineMock: Mocked<IMachineLearningRepository>;
let personMock: Mocked<IPersonRepository>; let personMock: Mocked<IPersonRepository>;
let searchMock: Mocked<ISearchRepository>; let searchMock: Mocked<ISearchRepository>;
let partnerMock: Mocked<IPartnerRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, assetMock, personMock, searchMock } = newTestService(SearchService));
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,
);
}); });
it('should work', () => { it('should work', () => {

View File

@ -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 { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto'; import { PersonResponseDto } from 'src/dtos/person.dto';
@ -16,34 +16,13 @@ import {
} from 'src/dtos/search.dto'; } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetOrder } from 'src/enum'; import { AssetOrder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { SearchExploreItem } from 'src/interfaces/search.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
import { isSmartSearchEnabled } from 'src/utils/misc'; import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable() @Injectable()
export class SearchService extends BaseService { 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<PersonResponseDto[]> { async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); 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 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 page = dto.page ?? 1;
const size = dto.size || 100; const size = dto.size || 100;
const { hasNextPage, items } = await this.searchRepository.searchSmart( const { hasNextPage, items } = await this.searchRepository.searchSmart(

View File

@ -1,41 +1,20 @@
import { SystemMetadataKey } from 'src/enum'; 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 { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { ServerService } from 'src/services/server.service'; import { ServerService } from 'src/services/server.service';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(ServerService.name, () => { describe(ServerService.name, () => {
let sut: ServerService; let sut: ServerService;
let configMock: Mocked<IConfigRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let userMock: Mocked<IUserRepository>;
let serverInfoMock: Mocked<IServerInfoRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>; let userMock: Mocked<IUserRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, storageMock, systemMock, userMock } = newTestService(ServerService));
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
serverInfoMock = newServerInfoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
loggerMock = newLoggerRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
sut = new ServerService(configMock, userMock, storageMock, systemMock, serverInfoMock, loggerMock, cryptoMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -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 { getBuildMetadata, getServerLicensePublicKey } from 'src/config';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
@ -15,13 +15,7 @@ import {
UsageByUserDto, UsageByUserDto,
} from 'src/dtos/server.dto'; } from 'src/dtos/server.dto';
import { StorageFolder, SystemMetadataKey } from 'src/enum'; import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { UserStatsQueryResponse } from 'src/interfaces/user.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { asHumanReadable } from 'src/utils/bytes'; import { asHumanReadable } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
@ -29,19 +23,6 @@ import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchE
@Injectable() @Injectable()
export class ServerService extends BaseService { 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' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap(): Promise<void> { async onBootstrap(): Promise<void> {
const featureFlags = await this.getFeatures(); const featureFlags = await this.getFeatures();

View File

@ -1,27 +1,21 @@
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { JobStatus } from 'src/interfaces/job.interface'; import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub'; import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newTestService } from 'test/utils';
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe('SessionService', () => { describe('SessionService', () => {
let sut: SessionService; let sut: SessionService;
let accessMock: Mocked<IAccessRepositoryMock>; let accessMock: Mocked<IAccessRepositoryMock>;
let loggerMock: Mocked<ILoggerRepository>;
let sessionMock: Mocked<ISessionRepository>; let sessionMock: Mocked<ISessionRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, sessionMock } = newTestService(SessionService));
loggerMock = newLoggerRepositoryMock();
sessionMock = newSessionRepositoryMock();
sut = new SessionService(accessMock, loggerMock, sessionMock);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -1,24 +1,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { JobStatus } from 'src/interfaces/job.interface'; import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { BaseService } from 'src/services/base.service';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class SessionService { export class SessionService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
) {
this.logger.setContext(SessionService.name);
}
async handleCleanup() { async handleCleanup() {
const sessions = await this.sessionRepository.search({ const sessions = await this.sessionRepository.search({
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(), updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
@ -44,7 +34,7 @@ export class SessionService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
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); await this.sessionRepository.delete(id);
} }

View File

@ -3,42 +3,24 @@ import _ from 'lodash';
import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { SharedLinkType } from 'src/enum'; 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 { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SharedLinkService } from 'src/services/shared-link.service'; import { SharedLinkService } from 'src/services/shared-link.service';
import { albumStub } from 'test/fixtures/album.stub'; import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(SharedLinkService.name, () => { describe(SharedLinkService.name, () => {
let sut: SharedLinkService; let sut: SharedLinkService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let configMock: Mocked<IConfigRepository>; let sharedLinkMock: Mocked<ISharedLinkRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let shareMock: Mocked<ISharedLinkRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let logMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, sharedLinkMock } = newTestService(SharedLinkService));
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
shareMock = newSharedLinkRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
logMock = newLoggerRepositoryMock();
sut = new SharedLinkService(accessMock, configMock, cryptoMock, logMock, shareMock, systemMock);
}); });
it('should work', () => { it('should work', () => {
@ -47,55 +29,55 @@ describe(SharedLinkService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it('should return all shared links for a user', async () => { 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([ await expect(sut.getAll(authStub.user1)).resolves.toEqual([
sharedLinkResponseStub.expired, sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid, sharedLinkResponseStub.valid,
]); ]);
expect(shareMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(sharedLinkMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
}); });
}); });
describe('getMine', () => { describe('getMine', () => {
it('should only work for a public user', async () => { it('should only work for a public user', async () => {
await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException); 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 () => { it('should return the shared link for the public user', async () => {
const authDto = authStub.adminSharedLink; const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.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 () => { it('should not return metadata', async () => {
const authDto = authStub.adminSharedLinkNoExif; const authDto = authStub.adminSharedLinkNoExif;
shareMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); 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 () => { it('should throw an error for an password protected shared link', async () => {
const authDto = authStub.adminSharedLink; const authDto = authStub.adminSharedLink;
shareMock.get.mockResolvedValue(sharedLinkStub.passwordRequired); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.passwordRequired);
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); 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', () => { describe('get', () => {
it('should throw an error for an invalid shared link', async () => { 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); await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled(); expect(sharedLinkMock.update).not.toHaveBeenCalled();
}); });
it('should get a shared link by id', async () => { 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); 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 () => { it('should create an album shared link', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); 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 }); await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id });
@ -134,7 +116,7 @@ describe(SharedLinkService.name, () => {
authStub.admin.user.id, authStub.admin.user.id,
new Set([albumStub.oneAsset.id]), new Set([albumStub.oneAsset.id]),
); );
expect(shareMock.create).toHaveBeenCalledWith({ expect(sharedLinkMock.create).toHaveBeenCalledWith({
type: SharedLinkType.ALBUM, type: SharedLinkType.ALBUM,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
albumId: albumStub.oneAsset.id, albumId: albumStub.oneAsset.id,
@ -150,7 +132,7 @@ describe(SharedLinkService.name, () => {
it('should create an individual shared link', async () => { it('should create an individual shared link', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
shareMock.create.mockResolvedValue(sharedLinkStub.individual); sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
await sut.create(authStub.admin, { await sut.create(authStub.admin, {
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
@ -164,7 +146,7 @@ describe(SharedLinkService.name, () => {
authStub.admin.user.id, authStub.admin.user.id,
new Set([assetStub.image.id]), new Set([assetStub.image.id]),
); );
expect(shareMock.create).toHaveBeenCalledWith({ expect(sharedLinkMock.create).toHaveBeenCalledWith({
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
albumId: null, 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 () => { 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])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
shareMock.create.mockResolvedValue(sharedLinkStub.individual); sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
await sut.create(authStub.admin, { await sut.create(authStub.admin, {
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
@ -194,7 +176,7 @@ describe(SharedLinkService.name, () => {
authStub.admin.user.id, authStub.admin.user.id,
new Set([assetStub.image.id]), new Set([assetStub.image.id]),
); );
expect(shareMock.create).toHaveBeenCalledWith({ expect(sharedLinkMock.create).toHaveBeenCalledWith({
type: SharedLinkType.INDIVIDUAL, type: SharedLinkType.INDIVIDUAL,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
albumId: null, albumId: null,
@ -211,18 +193,18 @@ describe(SharedLinkService.name, () => {
describe('update', () => { describe('update', () => {
it('should throw an error for an invalid shared link', async () => { 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); await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled(); expect(sharedLinkMock.update).not.toHaveBeenCalled();
}); });
it('should update a shared link', async () => { it('should update a shared link', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
shareMock.update.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.update.mockResolvedValue(sharedLinkStub.valid);
await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false }); await sut.update(authStub.user1, sharedLinkStub.valid.id, { allowDownload: false });
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(shareMock.update).toHaveBeenCalledWith({ expect(sharedLinkMock.update).toHaveBeenCalledWith({
id: sharedLinkStub.valid.id, id: sharedLinkStub.valid.id,
userId: authStub.user1.user.id, userId: authStub.user1.user.id,
allowDownload: false, allowDownload: false,
@ -232,31 +214,31 @@ describe(SharedLinkService.name, () => {
describe('remove', () => { describe('remove', () => {
it('should throw an error for an invalid shared link', async () => { 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); await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id'); expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(shareMock.update).not.toHaveBeenCalled(); expect(sharedLinkMock.update).not.toHaveBeenCalled();
}); });
it('should remove a key', async () => { it('should remove a key', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.valid); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.valid);
await sut.remove(authStub.user1, sharedLinkStub.valid.id); await sut.remove(authStub.user1, sharedLinkStub.valid.id);
expect(shareMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id); expect(sharedLinkMock.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(shareMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid); expect(sharedLinkMock.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
}); });
}); });
describe('addAssets', () => { describe('addAssets', () => {
it('should not work on album shared links', async () => { 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( await expect(sut.addAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
}); });
it('should add assets to a shared link', async () => { it('should add assets to a shared link', async () => {
shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
shareMock.create.mockResolvedValue(sharedLinkStub.individual); sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
await expect( await expect(
@ -268,7 +250,7 @@ describe(SharedLinkService.name, () => {
]); ]);
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1);
expect(shareMock.update).toHaveBeenCalledWith({ expect(sharedLinkMock.update).toHaveBeenCalledWith({
...sharedLinkStub.individual, ...sharedLinkStub.individual,
assets: [assetStub.image, { id: 'asset-3' }], assets: [assetStub.image, { id: 'asset-3' }],
}); });
@ -277,15 +259,15 @@ describe(SharedLinkService.name, () => {
describe('removeAssets', () => { describe('removeAssets', () => {
it('should not work on album shared links', async () => { 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( await expect(sut.removeAssets(authStub.admin, 'link-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
}); });
it('should remove assets from a shared link', async () => { it('should remove assets from a shared link', async () => {
shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); sharedLinkMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
shareMock.create.mockResolvedValue(sharedLinkStub.individual); sharedLinkMock.create.mockResolvedValue(sharedLinkStub.individual);
await expect( await expect(
sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }), 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 }, { 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', () => { describe('getMetadataTags', () => {
it('should return null when auth is not a shared link', async () => { it('should return null when auth is not a shared link', async () => {
await expect(sut.getMetadataTags(authStub.admin)).resolves.toBe(null); 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 () => { it('should return null when shared link has a password', async () => {
await expect(sut.getMetadataTags(authStub.passwordSharedLink)).resolves.toBe(null); 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 () => { it('should return metadata tags', async () => {
shareMock.get.mockResolvedValue(sharedLinkStub.individual); sharedLinkMock.get.mockResolvedValue(sharedLinkStub.individual);
await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({
description: '1 shared photos & videos', description: '1 shared photos & videos',
imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, imageUrl: `${DEFAULT_EXTERNAL_DOMAIN}/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`,
title: 'Public Share', title: 'Public Share',
}); });
expect(shareMock.get).toHaveBeenCalled(); expect(sharedLinkMock.get).toHaveBeenCalled();
}); });
}); });
}); });

View File

@ -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 { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants';
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto';
@ -14,32 +14,14 @@ import {
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { Permission, SharedLinkType } from 'src/enum'; 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 { BaseService } from 'src/services/base.service';
import { checkAccess, requireAccess } from 'src/utils/access'; import { checkAccess, requireAccess } from 'src/utils/access';
import { OpenGraphTags } from 'src/utils/misc'; import { OpenGraphTags } from 'src/utils/misc';
@Injectable() @Injectable()
export class SharedLinkService extends BaseService { 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<SharedLinkResponseDto[]> { getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
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<SharedLinkResponseDto> { async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
@ -67,7 +49,7 @@ export class SharedLinkService extends BaseService {
if (!dto.albumId) { if (!dto.albumId) {
throw new BadRequestException('Invalid 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; break;
} }
@ -76,13 +58,13 @@ export class SharedLinkService extends BaseService {
throw new BadRequestException('Invalid assetIds'); 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; break;
} }
} }
const sharedLink = await this.repository.create({ const sharedLink = await this.sharedLinkRepository.create({
key: this.cryptoRepository.randomBytes(50), key: this.cryptoRepository.randomBytes(50),
userId: auth.user.id, userId: auth.user.id,
type: dto.type, type: dto.type,
@ -101,7 +83,7 @@ export class SharedLinkService extends BaseService {
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) { async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
await this.findOrFail(auth.user.id, id); await this.findOrFail(auth.user.id, id);
const sharedLink = await this.repository.update({ const sharedLink = await this.sharedLinkRepository.update({
id, id,
userId: auth.user.id, userId: auth.user.id,
description: dto.description, description: dto.description,
@ -116,12 +98,12 @@ export class SharedLinkService extends BaseService {
async remove(auth: AuthDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
const sharedLink = await this.findOrFail(auth.user.id, id); 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 // TODO: replace `userId` with permissions and access control checks
private async findOrFail(userId: string, id: string) { 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) { if (!sharedLink) {
throw new BadRequestException('Shared link not found'); 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 existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id));
const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId));
const allowedAssetIds = await checkAccess(this.access, { const allowedAssetIds = await checkAccess(this.accessRepository, {
auth, auth,
permission: Permission.ASSET_SHARE, permission: Permission.ASSET_SHARE,
ids: notPresentAssetIds, ids: notPresentAssetIds,
@ -161,7 +143,7 @@ export class SharedLinkService extends BaseService {
sharedLink.assets.push({ id: assetId } as AssetEntity); sharedLink.assets.push({ id: assetId } as AssetEntity);
} }
await this.repository.update(sharedLink); await this.sharedLinkRepository.update(sharedLink);
return results; return results;
} }
@ -185,7 +167,7 @@ export class SharedLinkService extends BaseService {
sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId); sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== assetId);
} }
await this.repository.update(sharedLink); await this.sharedLinkRepository.update(sharedLink);
return results; return results;
} }

View File

@ -1,9 +1,6 @@
import { SystemConfig } from 'src/config'; import { SystemConfig } from 'src/config';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; 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 { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.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 { getCLIPModelInfo } from 'src/utils/misc';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(SmartInfoService.name, () => { describe(SmartInfoService.name, () => {
let sut: SmartInfoService; let sut: SmartInfoService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let machineLearningMock: Mocked<IMachineLearningRepository>;
let searchMock: Mocked<ISearchRepository>; let searchMock: Mocked<ISearchRepository>;
let machineMock: Mocked<IMachineLearningRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, assetMock, jobMock, machineLearningMock, searchMock, systemMock } = newTestService(SmartInfoService));
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,
);
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
}); });
@ -313,7 +283,7 @@ describe(SmartInfoService.name, () => {
expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED); expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(assetMock.getByIds).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled(); expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
}); });
it('should skip assets without a resize path', async () => { 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(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
expect(searchMock.upsert).not.toHaveBeenCalled(); expect(searchMock.upsert).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled(); expect(machineLearningMock.encodeImage).not.toHaveBeenCalled();
}); });
it('should save the returned objects', async () => { 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(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
expect(machineMock.encodeImage).toHaveBeenCalledWith( expect(machineLearningMock.encodeImage).toHaveBeenCalledWith(
'http://immich-machine-learning:3003', 'http://immich-machine-learning:3003',
'/uploads/user-id/thumbs/path.jpg', '/uploads/user-id/thumbs/path.jpg',
expect.objectContaining({ modelName: 'ViT-B-32__openai' }), 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(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(); expect(searchMock.upsert).not.toHaveBeenCalled();
}); });
}); });

View File

@ -1,23 +1,17 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config'; import { SystemConfig } from 'src/config';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseLock } from 'src/interfaces/database.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { import {
IBaseJob, IBaseJob,
IEntityJob, IEntityJob,
IJobRepository,
JOBS_ASSET_PAGINATION_SIZE, JOBS_ASSET_PAGINATION_SIZE,
JobName, JobName,
JobStatus, JobStatus,
QueueName, QueueName,
} from 'src/interfaces/job.interface'; } 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 { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
@ -25,20 +19,6 @@ import { usePagination } from 'src/utils/pagination';
@Injectable() @Injectable()
export class SmartInfoService extends BaseService { 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' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap(app: ArgOf<'app.bootstrap'>) { async onBootstrap(app: ArgOf<'app.bootstrap'>) {
if (app !== 'microservices') { if (app !== 'microservices') {
@ -72,7 +52,7 @@ export class SmartInfoService extends BaseService {
await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => { await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, async () => {
const { dimSize } = getCLIPModelInfo(newConfig.machineLearning.clip.modelName); 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}`); this.logger.verbose(`Current database CLIP dimension size is ${dbDimSize}`);
const modelChange = 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}.`, `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`,
); );
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); 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}.`); this.logger.log(`Successfully updated database CLIP dimension size from ${dbDimSize} to ${dimSize}.`);
} else { } else {
await this.repository.deleteAllSearchEmbeddings(); await this.searchRepository.deleteAllSearchEmbeddings();
} }
if (!isPaused) { if (!isPaused) {
@ -112,7 +92,7 @@ export class SmartInfoService extends BaseService {
} }
if (force) { if (force) {
await this.repository.deleteAllSearchEmbeddings(); await this.searchRepository.deleteAllSearchEmbeddings();
} }
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
@ -150,7 +130,7 @@ export class SmartInfoService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const embedding = await this.machineLearning.encodeImage( const embedding = await this.machineLearningRepository.encodeImage(
machineLearning.url, machineLearning.url,
previewFile.path, previewFile.path,
machineLearning.clip, machineLearning.clip,
@ -161,7 +141,7 @@ export class SmartInfoService extends BaseService {
await this.databaseRepository.wait(DatabaseLock.CLIPDimSize); await this.databaseRepository.wait(DatabaseLock.CLIPDimSize);
} }
await this.repository.upsert(asset.id, embedding); await this.searchRepository.upsert(asset.id, embedding);
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }

View File

@ -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 { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { BaseService } from 'src/services/base.service';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
@Injectable() @Injectable()
export class StackService { export class StackService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository,
) {}
async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> { async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> {
const stacks = await this.stackRepository.search({ const stacks = await this.stackRepository.search({
ownerId: auth.user.id, ownerId: auth.user.id,
@ -26,7 +18,7 @@ export class StackService {
} }
async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> { async create(auth: AuthDto, dto: StackCreateDto): Promise<StackResponseDto> {
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 }); 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<StackResponseDto> { async get(auth: AuthDto, id: string): Promise<StackResponseDto> {
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); const stack = await this.findOrFail(id);
return mapStack(stack, { auth }); return mapStack(stack, { auth });
} }
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> { async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
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); const stack = await this.findOrFail(id);
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack'); throw new BadRequestException('Primary asset must be in the stack');
@ -56,13 +48,13 @@ export class StackService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
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.stackRepository.delete(id);
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id }); await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
} }
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> { async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
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.stackRepository.deleteAll(dto.ids);
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id }); await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
} }

View File

@ -4,13 +4,9 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/enum'; import { AssetPathType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { JobStatus } from 'src/interfaces/job.interface'; import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMoveRepository } from 'src/interfaces/move.interface'; import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.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 { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(StorageTemplateService.name, () => { describe(StorageTemplateService.name, () => {
let sut: StorageTemplateService; let sut: StorageTemplateService;
let albumMock: Mocked<IAlbumRepository>; let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>; let cryptoMock: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let moveMock: Mocked<IMoveRepository>; let moveMock: Mocked<IMoveRepository>;
let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
it('should work', () => { it('should work', () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(() => { beforeEach(() => {
assetMock = newAssetRepositoryMock(); ({ sut, albumMock, assetMock, cryptoMock, moveMock, storageMock, systemMock, userMock } =
albumMock = newAlbumRepositoryMock(); newTestService(StorageTemplateService));
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } }); systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
sut = new StorageTemplateService(
albumMock,
assetMock,
configMock,
systemMock,
moveMock,
personMock,
storageMock,
userMock,
cryptoMock,
databaseMock,
loggerMock,
);
sut.onConfigUpdate({ newConfig: defaults }); sut.onConfigUpdate({ newConfig: defaults });
}); });

View File

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import handlebar from 'handlebars'; import handlebar from 'handlebars';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import path from 'node:path'; import path from 'node:path';
@ -16,19 +16,9 @@ import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum'; import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { DatabaseLock } from 'src/interfaces/database.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 { ArgOf } from 'src/interfaces/event.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.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 { BaseService } from 'src/services/base.service';
import { getLivePhotoMotionFilename } from 'src/utils/file'; import { getLivePhotoMotionFilename } from 'src/utils/file';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@ -47,7 +37,6 @@ interface RenderMetadata {
@Injectable() @Injectable()
export class StorageTemplateService extends BaseService { export class StorageTemplateService extends BaseService {
private storageCore: StorageCore;
private _template: { private _template: {
compiled: HandlebarsTemplateDelegate<any>; compiled: HandlebarsTemplateDelegate<any>;
raw: string; raw: string;
@ -61,33 +50,6 @@ export class StorageTemplateService extends BaseService {
return this._template; 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 }) @OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
const template = newConfig.storageTemplate.template; const template = newConfig.storageTemplate.template;

View File

@ -1,33 +1,21 @@
import { SystemMetadataKey } from 'src/enum'; import { SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface'; 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 { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { StorageService } from 'src/services/storage.service'; import { StorageService } from 'src/services/storage.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(StorageService.name, () => { describe(StorageService.name, () => {
let sut: StorageService; let sut: StorageService;
let configMock: Mocked<IConfigRepository>; let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, configMock, storageMock, systemMock } = newTestService(StorageService));
databaseMock = newDatabaseRepositoryMock();
storageMock = newStorageRepositoryMock();
loggerMock = newLoggerRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
sut = new StorageService(configMock, databaseMock, storageMock, loggerMock, systemMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,36 +1,23 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { join } from 'node:path'; import { join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { StorageFolder, SystemMetadataKey } from 'src/enum'; import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseLock } from 'src/interfaces/database.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { BaseService } from 'src/services/base.service';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ImmichStartupError } from 'src/utils/events'; import { ImmichStartupError } from 'src/utils/events';
const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`; const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
@Injectable() @Injectable()
export class StorageService { export class StorageService extends BaseService {
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);
}
@OnEvent({ name: 'app.bootstrap' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap() { async onBootstrap() {
const envData = this.configRepository.getEnv(); const envData = this.configRepository.getEnv();
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { 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; const enabled = flags.mountFiles ?? false;
this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`); this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`);
@ -49,7 +36,7 @@ export class StorageService {
if (!flags.mountFiles) { if (!flags.mountFiles) {
flags.mountFiles = true; 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'); this.logger.log('Successfully enabled system mount folders checks');
} }

View File

@ -1,6 +1,5 @@
import { mapAsset } from 'src/dtos/asset-response.dto'; import { mapAsset } from 'src/dtos/asset-response.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IPartnerRepository } from 'src/interfaces/partner.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 { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.stub'; import { partnerStub } from 'test/fixtures/partner.stub';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const untilDate = new Date(2024); const untilDate = new Date(2024);
@ -19,17 +15,13 @@ const mapAssetOpts = { auth: authStub.user1, stripMetadata: false, withStack: tr
describe(SyncService.name, () => { describe(SyncService.name, () => {
let sut: SyncService; let sut: SyncService;
let accessMock: Mocked<IAccessRepository>;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let partnerMock: Mocked<IPartnerRepository>;
let auditMock: Mocked<IAuditRepository>; let auditMock: Mocked<IAuditRepository>;
let partnerMock: Mocked<IPartnerRepository>;
beforeEach(() => { beforeEach(() => {
partnerMock = newPartnerRepositoryMock(); ({ sut, assetMock, auditMock, partnerMock } = newTestService(SyncService));
assetMock = newAssetRepositoryMock();
accessMock = newAccessRepositoryMock();
auditMock = newAuditRepositoryMock();
sut = new SyncService(accessMock, assetMock, partnerMock, auditMock);
}); });
it('should exist', () => { it('should exist', () => {

View File

@ -1,32 +1,21 @@
import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto';
import { DatabaseAction, EntityType, Permission } from 'src/enum'; import { DatabaseAction, EntityType, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { BaseService } from 'src/services/base.service';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
import { setIsEqual } from 'src/utils/set'; import { setIsEqual } from 'src/utils/set';
const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] };
export class SyncService { export class SyncService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IAuditRepository) private auditRepository: IAuditRepository,
) {}
async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> { async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise<AssetResponseDto[]> {
// mobile implementation is faster if this is a single id // mobile implementation is faster if this is a single id
const userId = dto.userId || auth.user.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({ const assets = await this.assetRepository.getAllForUserFullSync({
ownerId: userId, ownerId: userId,
updatedUntil: dto.updatedUntil, updatedUntil: dto.updatedUntil,
@ -50,7 +39,7 @@ export class SyncService {
return FULL_SYNC; 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 limit = 10_000;
const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds });

View File

@ -18,10 +18,8 @@ import { QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemConfigService } from 'src/services/system-config.service'; import { SystemConfigService } from 'src/services/system-config.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newTestService } from 'test/utils';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
@ -189,18 +187,14 @@ const updatedConfig = Object.freeze<SystemConfig>({
describe(SystemConfigService.name, () => { describe(SystemConfigService.name, () => {
let sut: SystemConfigService; let sut: SystemConfigService;
let configMock: Mocked<IConfigRepository>; let configMock: Mocked<IConfigRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let loggerMock: Mocked<ILoggerRepository>; let loggerMock: Mocked<ILoggerRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, configMock, eventMock, loggerMock, systemMock } = newTestService(SystemConfigService));
eventMock = newEventRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new SystemConfigService(configMock, eventMock, systemMock, loggerMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { instanceToPlain } from 'class-transformer'; import { instanceToPlain } from 'class-transformer';
import _ from 'lodash'; import _ from 'lodash';
import { defaults } from 'src/config'; import { defaults } from 'src/config';
@ -14,26 +14,13 @@ import {
} from 'src/constants'; } from 'src/constants';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { clearConfigCache } from 'src/utils/config'; import { clearConfigCache } from 'src/utils/config';
import { toPlainObject } from 'src/utils/object'; import { toPlainObject } from 'src/utils/object';
@Injectable() @Injectable()
export class SystemConfigService extends BaseService { 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 }) @OnEvent({ name: 'app.bootstrap', priority: -100 })
async onBootstrap() { async onBootstrap() {
const config = await this.getConfig({ withCache: false }); const config = await this.getConfig({ withCache: false });

View File

@ -1,16 +1,15 @@
import { SystemMetadataKey } from 'src/enum'; import { SystemMetadataKey } from 'src/enum';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemMetadataService } from 'src/services/system-metadata.service'; 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'; import { Mocked } from 'vitest';
describe(SystemMetadataService.name, () => { describe(SystemMetadataService.name, () => {
let sut: SystemMetadataService; let sut: SystemMetadataService;
let metadataMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => { beforeEach(() => {
metadataMock = newSystemMetadataRepositoryMock(); ({ sut, systemMock } = newTestService(SystemMetadataService));
sut = new SystemMetadataService(metadataMock);
}); });
it('should work', () => { it('should work', () => {
@ -20,12 +19,12 @@ describe(SystemMetadataService.name, () => {
describe('updateAdminOnboarding', () => { describe('updateAdminOnboarding', () => {
it('should update isOnboarded to true', async () => { it('should update isOnboarded to true', async () => {
await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined(); 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 () => { it('should update isOnboarded to false', async () => {
await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined(); 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 });
}); });
}); });
}); });

View File

@ -1,29 +1,27 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
AdminOnboardingResponseDto, AdminOnboardingResponseDto,
AdminOnboardingUpdateDto, AdminOnboardingUpdateDto,
ReverseGeocodingStateResponseDto, ReverseGeocodingStateResponseDto,
} from 'src/dtos/system-metadata.dto'; } from 'src/dtos/system-metadata.dto';
import { SystemMetadataKey } from 'src/enum'; import { SystemMetadataKey } from 'src/enum';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { BaseService } from 'src/services/base.service';
@Injectable() @Injectable()
export class SystemMetadataService { export class SystemMetadataService extends BaseService {
constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {}
async getAdminOnboarding(): Promise<AdminOnboardingResponseDto> { async getAdminOnboarding(): Promise<AdminOnboardingResponseDto> {
const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING); const value = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
return { isOnboarded: false, ...value }; return { isOnboarded: false, ...value };
} }
async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise<void> { async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise<void> {
await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, { await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
isOnboarded: dto.isOnboarded, isOnboarded: dto.isOnboarded,
}); });
} }
async getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> { async getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
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 }; return { lastUpdate: null, lastImportFileName: null, ...value };
} }
} }

View File

@ -1,26 +1,21 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ITagRepository } from 'src/interfaces/tag.interface'; import { ITagRepository } from 'src/interfaces/tag.interface';
import { TagService } from 'src/services/tag.service'; import { TagService } from 'src/services/tag.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newTestService } from 'test/utils';
import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(TagService.name, () => { describe(TagService.name, () => {
let sut: TagService; let sut: TagService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let eventMock: Mocked<IEventRepository>;
let tagMock: Mocked<ITagRepository>; let tagMock: Mocked<ITagRepository>;
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, tagMock } = newTestService(TagService));
eventMock = newEventRepositoryMock();
tagMock = newTagRepositoryMock();
sut = new TagService(accessMock, eventMock, tagMock);
accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1']));
}); });

View File

@ -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 { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
@ -12,29 +12,22 @@ import {
} from 'src/dtos/tag.dto'; } from 'src/dtos/tag.dto';
import { TagEntity } from 'src/entities/tag.entity'; import { TagEntity } from 'src/entities/tag.entity';
import { Permission } from 'src/enum'; 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 { 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 { checkAccess, requireAccess } from 'src/utils/access';
import { addAssets, removeAssets } from 'src/utils/asset.util'; import { addAssets, removeAssets } from 'src/utils/asset.util';
import { upsertTags } from 'src/utils/tag'; import { upsertTags } from 'src/utils/tag';
@Injectable() @Injectable()
export class TagService { export class TagService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ITagRepository) private repository: ITagRepository,
) {}
async getAll(auth: AuthDto) { 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)); return tags.map((tag) => mapTag(tag));
} }
async get(auth: AuthDto, id: string): Promise<TagResponseDto> { async get(auth: AuthDto, id: string): Promise<TagResponseDto> {
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); const tag = await this.findOrFail(id);
return mapTag(tag); return mapTag(tag);
} }
@ -42,8 +35,8 @@ export class TagService {
async create(auth: AuthDto, dto: TagCreateDto) { async create(auth: AuthDto, dto: TagCreateDto) {
let parent: TagEntity | undefined; let parent: TagEntity | undefined;
if (dto.parentId) { if (dto.parentId) {
await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); await requireAccess(this.accessRepository, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] });
parent = (await this.repository.get(dto.parentId)) || undefined; parent = (await this.tagRepository.get(dto.parentId)) || undefined;
if (!parent) { if (!parent) {
throw new BadRequestException('Tag not found'); throw new BadRequestException('Tag not found');
} }
@ -51,41 +44,41 @@ export class TagService {
const userId = auth.user.id; const userId = auth.user.id;
const value = parent ? `${parent.value}/${dto.name}` : dto.name; 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) { if (duplicate) {
throw new BadRequestException(`A tag with that name already exists`); 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); return mapTag(tag);
} }
async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> { async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise<TagResponseDto> {
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 { color } = dto;
const tag = await this.repository.update({ id, color }); const tag = await this.tagRepository.update({ id, color });
return mapTag(tag); return mapTag(tag);
} }
async upsert(auth: AuthDto, dto: TagUpsertDto) { 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)); return tags.map((tag) => mapTag(tag));
} }
async remove(auth: AuthDto, id: string): Promise<void> { async remove(auth: AuthDto, id: string): Promise<void> {
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 // TODO sync tag changes for affected assets
await this.repository.delete(id); await this.tagRepository.delete(id);
} }
async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> { async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
const [tagIds, assetIds] = await Promise.all([ const [tagIds, assetIds] = await Promise.all([
checkAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), checkAccess(this.accessRepository, { 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.ASSET_UPDATE, ids: dto.assetIds }),
]); ]);
const items: AssetTagItem[] = []; 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))) { for (const assetId of new Set(results.map((item) => item.assetId))) {
await this.eventRepository.emit('asset.tag', { assetId }); await this.eventRepository.emit('asset.tag', { assetId });
} }
@ -104,11 +97,11 @@ export class TagService {
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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( const results = await addAssets(
auth, auth,
{ access: this.access, bulk: this.repository }, { access: this.accessRepository, bulk: this.tagRepository },
{ parentId: id, assetIds: dto.ids }, { parentId: id, assetIds: dto.ids },
); );
@ -122,11 +115,11 @@ export class TagService {
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
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( const results = await removeAssets(
auth, auth,
{ access: this.access, bulk: this.repository }, { access: this.accessRepository, bulk: this.tagRepository },
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE },
); );
@ -140,12 +133,12 @@ export class TagService {
} }
async handleTagCleanup() { async handleTagCleanup() {
await this.repository.deleteEmptyTags(); await this.tagRepository.deleteEmptyTags();
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
private async findOrFail(id: string) { private async findOrFail(id: string) {
const tag = await this.repository.get(id); const tag = await this.tagRepository.get(id);
if (!tag) { if (!tag) {
throw new BadRequestException('Tag not found'); throw new BadRequestException('Tag not found');
} }

View File

@ -1,25 +1,20 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface'; import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { TimelineService } from 'src/services/timeline.service'; import { TimelineService } from 'src/services/timeline.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newTestService } from 'test/utils';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(TimelineService.name, () => { describe(TimelineService.name, () => {
let sut: TimelineService; let sut: TimelineService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
let partnerMock: Mocked<IPartnerRepository>;
beforeEach(() => {
accessMock = newAccessRepositoryMock();
assetMock = newAssetRepositoryMock();
partnerMock = newPartnerRepositoryMock();
sut = new TimelineService(accessMock, assetMock, partnerMock); beforeEach(() => {
({ sut, accessMock, assetMock } = newTestService(TimelineService));
}); });
describe('getTimeBuckets', () => { describe('getTimeBuckets', () => {

View File

@ -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 { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { TimeBucketOptions } from 'src/interfaces/asset.interface';
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { BaseService } from 'src/services/base.service';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
export class TimelineService { export class TimelineService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private repository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
) {}
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
await this.timeBucketChecks(auth, dto); await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
return this.assetRepository.getTimeBuckets(timeBucketOptions);
return this.repository.getTimeBuckets(timeBucketOptions);
} }
async getTimeBucket( async getTimeBucket(
@ -29,7 +21,7 @@ export class TimelineService {
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> { ): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(auth, dto); await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(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 return !auth.sharedLink || auth.sharedLink?.showExif
? assets.map((asset) => mapAsset(asset, { withStack: true, auth })) ? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth })); : assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
@ -56,20 +48,20 @@ export class TimelineService {
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.albumId) { 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 { } else {
dto.userId = dto.userId || auth.user.id; dto.userId = dto.userId || auth.user.id;
} }
if (dto.userId) { 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) { 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) { 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) { if (dto.withPartners) {

View File

@ -1,37 +1,25 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.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 { ITrashRepository } from 'src/interfaces/trash.interface';
import { TrashService } from 'src/services/trash.service'; import { TrashService } from 'src/services/trash.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
describe(TrashService.name, () => { describe(TrashService.name, () => {
let sut: TrashService; let sut: TrashService;
let accessMock: IAccessRepositoryMock; let accessMock: IAccessRepositoryMock;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let trashMock: Mocked<ITrashRepository>; let trashMock: Mocked<ITrashRepository>;
let loggerMock: Mocked<ILoggerRepository>;
it('should work', () => { it('should work', () => {
expect(sut).toBeDefined(); expect(sut).toBeDefined();
}); });
beforeEach(() => { beforeEach(() => {
accessMock = newAccessRepositoryMock(); ({ sut, accessMock, jobMock, trashMock } = newTestService(TrashService));
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
trashMock = newTrashRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new TrashService(accessMock, eventMock, jobMock, trashMock, loggerMock);
}); });
describe('restoreAssets', () => { describe('restoreAssets', () => {

View File

@ -1,35 +1,21 @@
import { Inject } from '@nestjs/common';
import { OnEvent } from 'src/decorators'; import { OnEvent } from 'src/decorators';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TrashResponseDto } from 'src/dtos/trash.dto'; import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum'; import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { BaseService } from 'src/services/base.service';
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 { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
export class TrashService { export class TrashService extends BaseService {
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);
}
async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<TrashResponseDto> { async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise<TrashResponseDto> {
const { ids } = dto; const { ids } = dto;
if (ids.length === 0) { if (ids.length === 0) {
return { count: 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.trashRepository.restoreAll(ids);
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id }); await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });

View File

@ -1,41 +1,22 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { mapUserAdmin } from 'src/dtos/user.dto'; import { mapUserAdmin } from 'src/dtos/user.dto';
import { UserStatus } from 'src/enum'; 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 { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { UserAdminService } from 'src/services/user-admin.service'; import { UserAdminService } from 'src/services/user-admin.service';
import { authStub } from 'test/fixtures/auth.stub'; import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked, describe } from 'vitest'; import { Mocked, describe } from 'vitest';
describe(UserAdminService.name, () => { describe(UserAdminService.name, () => {
let sut: UserAdminService; let sut: UserAdminService;
let albumMock: Mocked<IAlbumRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let userMock: Mocked<IUserRepository>; let userMock: Mocked<IUserRepository>;
beforeEach(() => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); ({ sut, jobMock, userMock } = newTestService(UserAdminService));
cryptoMock = newCryptoRepositoryMock();
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new UserAdminService(albumMock, cryptoMock, eventMock, jobMock, userMock, loggerMock);
userMock.get.mockImplementation((userId) => userMock.get.mockImplementation((userId) =>
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),

View File

@ -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 { SALT_ROUNDS } from 'src/constants';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
@ -11,28 +11,14 @@ import {
mapUserAdmin, mapUserAdmin,
} from 'src/dtos/user.dto'; } from 'src/dtos/user.dto';
import { UserMetadataKey, UserStatus } from 'src/enum'; import { UserMetadataKey, UserStatus } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { JobName } from 'src/interfaces/job.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { UserFindOptions } from 'src/interfaces/user.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { BaseService } from 'src/services/base.service';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
import { createUser } from 'src/utils/user'; import { createUser } from 'src/utils/user';
@Injectable() @Injectable()
export class UserAdminService { export class UserAdminService extends BaseService {
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);
}
async search(auth: AuthDto, dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> { async search(auth: AuthDto, dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: dto.withDeleted }); const users = await this.userRepository.getList({ withDeleted: dto.withDeleted });
return users.map((user) => mapUserAdmin(user)); return users.map((user) => mapUserAdmin(user));

View File

@ -2,10 +2,7 @@ import { BadRequestException, InternalServerErrorException, NotFoundException }
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, UserMetadataKey } from 'src/enum'; import { CacheControl, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; 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 { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.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 { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const makeDeletedAt = (daysAgo: number) => { const makeDeletedAt = (daysAgo: number) => {
@ -32,36 +22,15 @@ const makeDeletedAt = (daysAgo: number) => {
describe(UserService.name, () => { describe(UserService.name, () => {
let sut: UserService; let sut: UserService;
let userMock: Mocked<IUserRepository>;
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
let albumMock: Mocked<IAlbumRepository>; let albumMock: Mocked<IAlbumRepository>;
let configMock: Mocked<IConfigRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let storageMock: Mocked<IStorageRepository>; let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>; let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>; let userMock: Mocked<IUserRepository>;
beforeEach(() => { beforeEach(() => {
albumMock = newAlbumRepositoryMock(); ({ sut, albumMock, jobMock, storageMock, systemMock, userMock } = newTestService(UserService));
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,
);
userMock.get.mockImplementation((userId) => userMock.get.mockImplementation((userId) =>
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null), Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),

View File

@ -1,4 +1,4 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config'; import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
import { SALT_ROUNDS } from 'src/constants'; 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 { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum'; import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IEntityJob, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { UserFindOptions } from 'src/interfaces/user.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { ImmichFileResponse } from 'src/utils/file'; import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable() @Injectable()
export class UserService extends BaseService { 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<UserResponseDto[]> { async search(): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: false }); const users = await this.userRepository.getList({ withDeleted: false });
return users.map((user) => mapUser(user)); return users.map((user) => mapUser(user));

View File

@ -2,7 +2,6 @@ import { DateTime } from 'luxon';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { IConfigRepository } from 'src/interfaces/config.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface'; import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newTestService } from 'test/utils';
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 { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const mockRelease = (version: string) => ({ const mockRelease = (version: string) => ({
@ -32,35 +25,18 @@ const mockRelease = (version: string) => ({
describe(VersionService.name, () => { describe(VersionService.name, () => {
let sut: VersionService; let sut: VersionService;
let configMock: Mocked<IConfigRepository>; let configMock: Mocked<IConfigRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let eventMock: Mocked<IEventRepository>; let eventMock: Mocked<IEventRepository>;
let jobMock: Mocked<IJobRepository>; let jobMock: Mocked<IJobRepository>;
let serverMock: Mocked<IServerInfoRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let versionMock: Mocked<IVersionHistoryRepository>;
let loggerMock: Mocked<ILoggerRepository>; let loggerMock: Mocked<ILoggerRepository>;
let serverInfoMock: Mocked<IServerInfoRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let versionHistoryMock: Mocked<IVersionHistoryRepository>;
beforeEach(() => { beforeEach(() => {
configMock = newConfigRepositoryMock(); ({ sut, configMock, eventMock, jobMock, loggerMock, serverInfoMock, systemMock, versionHistoryMock } =
databaseMock = newDatabaseRepositoryMock(); newTestService(VersionService));
eventMock = newEventRepositoryMock();
jobMock = newJobRepositoryMock();
serverMock = newServerInfoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
versionMock = newVersionHistoryRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new VersionService(
configMock,
databaseMock,
eventMock,
jobMock,
serverMock,
systemMock,
versionMock,
loggerMock,
);
}); });
it('should work', () => { it('should work', () => {
@ -70,17 +46,17 @@ describe(VersionService.name, () => {
describe('onBootstrap', () => { describe('onBootstrap', () => {
it('should record a new version', async () => { it('should record a new version', async () => {
await expect(sut.onBootstrap()).resolves.toBeUndefined(); 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 () => { it('should skip a duplicate version', async () => {
versionMock.getLatest.mockResolvedValue({ versionHistoryMock.getLatest.mockResolvedValue({
id: 'version-1', id: 'version-1',
createdAt: new Date(), createdAt: new Date(),
version: serverVersion.toString(), version: serverVersion.toString(),
}); });
await expect(sut.onBootstrap()).resolves.toBeUndefined(); 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', () => { describe('getVersionHistory', () => {
it('should respond the server version history', async () => { it('should respond the server version history', async () => {
const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' }; 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]); await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]);
}); });
}); });
@ -128,7 +104,7 @@ describe(VersionService.name, () => {
}); });
it('should run if it has been > 60 minutes', async () => { 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({ systemMock.get.mockResolvedValue({
checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(), checkedAt: DateTime.utc().minus({ minutes: 65 }).toISO(),
releaseVersion: '1.0.0', releaseVersion: '1.0.0',
@ -140,7 +116,7 @@ describe(VersionService.name, () => {
}); });
it('should not notify if the version is equal', async () => { 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); await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.SUCCESS);
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, { expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.VERSION_CHECK_STATE, {
checkedAt: expect.any(String), checkedAt: expect.any(String),
@ -150,7 +126,7 @@ describe(VersionService.name, () => {
}); });
it('should handle a github error', async () => { 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); await expect(sut.handleVersionCheck()).resolves.toEqual(JobStatus.FAILED);
expect(systemMock.set).not.toHaveBeenCalled(); expect(systemMock.set).not.toHaveBeenCalled();
expect(eventMock.clientBroadcast).not.toHaveBeenCalled(); expect(eventMock.clientBroadcast).not.toHaveBeenCalled();

View File

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import semver, { SemVer } from 'semver'; import semver, { SemVer } from 'semver';
import { serverVersion } from 'src/constants'; import { serverVersion } from 'src/constants';
@ -6,14 +6,9 @@ import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum'; import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseLock } from 'src/interfaces/database.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; import { JobName, JobStatus } from 'src/interfaces/job.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 { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
@ -27,20 +22,6 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
@Injectable() @Injectable()
export class VersionService extends BaseService { 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' }) @OnEvent({ name: 'app.bootstrap' })
async onBootstrap(): Promise<void> { async onBootstrap(): Promise<void> {
await this.handleVersionCheck(); 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 }; const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata); await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata);

View File

@ -3,7 +3,7 @@ import { IViewRepository } from 'src/interfaces/view.interface';
import { ViewService } from 'src/services/view.service'; import { ViewService } from 'src/services/view.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.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'; import { Mocked } from 'vitest';
@ -12,9 +12,7 @@ describe(ViewService.name, () => {
let viewMock: Mocked<IViewRepository>; let viewMock: Mocked<IViewRepository>;
beforeEach(() => { beforeEach(() => {
viewMock = newViewRepositoryMock(); ({ sut, viewMock } = newTestService(ViewService));
sut = new ViewService(viewMock);
}); });
it('should work', () => { it('should work', () => {

View File

@ -1,11 +1,8 @@
import { Inject } from '@nestjs/common';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { IViewRepository } from 'src/interfaces/view.interface'; import { BaseService } from 'src/services/base.service';
export class ViewService {
constructor(@Inject(IViewRepository) private viewRepository: IViewRepository) {}
export class ViewService extends BaseService {
getUniqueOriginalPaths(auth: AuthDto): Promise<string[]> { getUniqueOriginalPaths(auth: AuthDto): Promise<string[]> {
return this.viewRepository.getUniqueOriginalPaths(auth.user.id); return this.viewRepository.getUniqueOriginalPaths(auth.user.id);
} }

160
server/test/utils.ts Normal file
View File

@ -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<typeof BaseService>;
type Constructor<Type, Args extends Array<any>> = {
new (...deps: Args): Type;
};
export const newTestService = <T extends BaseService>(Service: Constructor<T, BaseServiceArgs>) => {
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,
};
};