1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-16 00:36:47 +01:00

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 { activityStub } from 'test/fixtures/activity.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newActivityRepositoryMock } from 'test/repositories/activity.repository.mock';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
describe(ActivityService.name, () => {
let sut: ActivityService;
let accessMock: IAccessRepositoryMock;
let activityMock: Mocked<IActivityRepository>;
beforeEach(() => {
accessMock = newAccessRepositoryMock();
activityMock = newActivityRepositoryMock();
sut = new ActivityService(accessMock, activityMock);
({ sut, accessMock, activityMock } = newTestService(ActivityService));
});
it('should work', () => {

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { BadRequestException, Inject } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
import _ from 'lodash';
import { DateTime, Duration } from 'luxon';
import {
@ -20,46 +20,20 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { MemoryLaneDto } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
IAssetDeleteJob,
IJobRepository,
ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
JobStatus,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access';
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination';
export class AssetService extends BaseService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(AssetService.name);
}
async getMemoryLane(auth: AuthDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const partnerIds = await getMyPartnerIds({
userId: auth.user.id,
@ -112,7 +86,7 @@ export class AssetService extends BaseService {
}
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(
id,
@ -161,7 +135,7 @@ export class AssetService extends BaseService {
}
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 repos = { asset: this.assetRepository, event: this.eventRepository };
@ -204,7 +178,7 @@ export class AssetService extends BaseService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids });
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids });
for (const id of ids) {
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
@ -301,7 +275,7 @@ export class AssetService extends BaseService {
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto;
await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids });
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_DELETE, ids });
await this.assetRepository.updateAll(ids, {
deletedAt: new Date(),
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
@ -310,7 +284,7 @@ export class AssetService extends BaseService {
}
async run(auth: AuthDto, dto: AssetJobsDto) {
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
await requireAccess(this.accessRepository, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds });
const jobs: JobItem[] = [];

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import {
BadRequestException,
ForbiddenException,
Inject,
Injectable,
InternalServerErrorException,
UnauthorizedException,
@ -13,6 +12,7 @@ import { IncomingHttpHeaders } from 'node:http';
import { Issuer, UserinfoResponse, custom, generators } from 'openid-client';
import { SystemConfig } from 'src/config';
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
import { OnEvent } from 'src/decorators';
import {
AuthDto,
ChangePasswordDto,
@ -30,15 +30,6 @@ import {
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes';
@ -72,20 +63,8 @@ export type ValidateRequest = {
@Injectable()
export class AuthService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(AuthService.name);
@OnEvent({ name: 'app.bootstrap' })
onBootstrap() {
custom.setHttpOptionsDefaults({ timeout: 30_000 });
}

View file

@ -1,16 +1,97 @@
import { Inject } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IActivityRepository } from 'src/interfaces/activity.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IAuditRepository } from 'src/interfaces/audit.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMemoryRepository } from 'src/interfaces/memory.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMetricRepository } from 'src/interfaces/metric.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { IViewRepository } from 'src/interfaces/view.interface';
import { getConfig, updateConfig } from 'src/utils/config';
export class BaseService {
protected storageCore: StorageCore;
constructor(
@Inject(IConfigRepository) protected configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@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() {
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 { CliService } from 'src/services/cli.service';
import { userStub } from 'test/fixtures/user.stub';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked, describe, it } from 'vitest';
describe(CliService.name, () => {
let sut: CliService;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let userMock: Mocked<IUserRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new CliService(configMock, cryptoMock, systemMock, userMock, loggerMock);
({ sut, userMock } = newTestService(CliService));
});
describe('resetAdminPassword', () => {

View file

@ -1,26 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class CliService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(CliService.name);
}
async listUsers(): Promise<UserAdminResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: true });
return users.map((user) => mapUserAdmin(user));

View file

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

View file

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

View file

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

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

View file

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

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 { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
IBaseJob,
IEntityJob,
IJobRepository,
JOBS_ASSET_PAGINATION_SIZE,
JobName,
JobStatus,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { AssetDuplicateResult } from 'src/interfaces/search.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { isDuplicateDetectionEnabled } from 'src/utils/misc';
@ -24,19 +13,6 @@ import { usePagination } from 'src/utils/pagination';
@Injectable()
export class DuplicateService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(DuplicateService.name);
}
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });

View file

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

View file

@ -1,15 +1,12 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { snakeCase } from 'lodash';
import { OnEvent } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ManualJobName } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import {
ConcurrentQueueName,
IJobRepository,
JobCommand,
JobHandler,
JobItem,
@ -18,10 +15,6 @@ import {
QueueCleanType,
QueueName,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMetricRepository } from 'src/interfaces/metric.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseService } from 'src/services/base.service';
const asJobItem = (dto: JobCreateDto): JobItem => {
@ -48,20 +41,6 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
export class JobService extends BaseService {
private isMicroservices = false;
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IMetricRepository) private metricRepository: IMetricRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(JobService.name);
}
@OnEvent({ name: 'app.bootstrap' })
onBootstrap(app: ArgOf<'app.bootstrap'>) {
this.isMicroservices = app === 'microservices';

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { dirname } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
@ -17,31 +17,17 @@ import {
VideoCodec,
VideoContainer,
} from 'src/enum';
import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
import {
IBaseJob,
IEntityJob,
IJobRepository,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
AudioStreamInfo,
IMediaRepository,
TranscodeCommand,
VideoFormat,
VideoStreamInfo,
} from 'src/interfaces/media.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { AudioStreamInfo, TranscodeCommand, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media';
@ -50,36 +36,9 @@ import { usePagination } from 'src/utils/pagination';
@Injectable()
export class MediaService extends BaseService {
private storageCore: StorageCore;
private maliOpenCL?: boolean;
private devices?: string[];
constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(MediaService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,
storageRepository,
systemMetadataRepository,
this.logger,
);
}
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force

View file

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

View file

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

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 { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import _ from 'lodash';
@ -13,32 +13,20 @@ import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import {
IBaseJob,
IEntityJob,
IJobRepository,
ISidecarWriteJob,
JobName,
JOBS_ASSET_PAGINATION_SIZE,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMapRepository, ReverseGeocodeResult } from 'src/interfaces/map.interface';
import { IMediaRepository } from 'src/interfaces/media.interface';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { ReverseGeocodeResult } from 'src/interfaces/map.interface';
import { ImmichTags } from 'src/interfaces/metadata.interface';
import { BaseService } from 'src/services/base.service';
import { isFaceImportEnabled } from 'src/utils/misc';
import { usePagination } from 'src/utils/pagination';
@ -99,41 +87,6 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
@Injectable()
export class MetadataService extends BaseService {
private storageCore: StorageCore;
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IMapRepository) private mapRepository: IMapRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@Inject(IMetadataRepository) private repository: IMetadataRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ITagRepository) private tagRepository: ITagRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(MetadataService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,
storageRepository,
systemMetadataRepository,
this.logger,
);
}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
if (app !== 'microservices') {
@ -145,7 +98,7 @@ export class MetadataService extends BaseService {
@OnEvent({ name: 'app.shutdown' })
async onShutdown() {
await this.repository.teardown();
await this.metadataRepository.teardown();
}
@OnEvent({ name: 'config.update' })
@ -372,7 +325,7 @@ export class MetadataService extends BaseService {
return JobStatus.SKIPPED;
}
await this.repository.writeTags(sidecarPath, exif);
await this.metadataRepository.writeTags(sidecarPath, exif);
if (!asset.sidecarPath) {
await this.assetRepository.update({ id, sidecarPath });
@ -382,8 +335,8 @@ export class MetadataService extends BaseService {
}
private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
const mediaTags = await this.repository.readTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : {};
const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};
const videoTags = asset.type === AssetType.VIDEO ? await this.getVideoTags(asset.originalPath) : {};
// make sure dates comes from sidecar
@ -467,11 +420,11 @@ export class MetadataService extends BaseService {
// Samsung MotionPhoto video extraction
// HEIC-encoded
if (hasMotionPhotoVideo) {
video = await this.repository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
}
// JPEG-encoded; HEIC also contains these tags, so this conditional must come second
else if (hasEmbeddedVideoFile) {
video = await this.repository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
}
// Default video extraction
else {

View file

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

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

View file

@ -1,20 +1,17 @@
import { BadRequestException } from '@nestjs/common';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
import { PartnerService } from 'src/services/partner.service';
import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.stub';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
describe(PartnerService.name, () => {
let sut: PartnerService;
let partnerMock: Mocked<IPartnerRepository>;
let accessMock: Mocked<IAccessRepository>;
beforeEach(() => {
partnerMock = newPartnerRepositoryMock();
sut = new PartnerService(partnerMock, accessMock);
({ sut, partnerMock } = newTestService(PartnerService));
});
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 { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
import { mapUser } from 'src/dtos/user.dto';
import { PartnerEntity } from 'src/entities/partner.entity';
import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
import { PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access';
@Injectable()
export class PartnerService {
constructor(
@Inject(IPartnerRepository) private repository: IPartnerRepository,
@Inject(IAccessRepository) private access: IAccessRepository,
) {}
export class PartnerService extends BaseService {
async create(auth: AuthDto, sharedWithId: string): Promise<PartnerResponseDto> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const exists = await this.repository.get(partnerId);
const exists = await this.partnerRepository.get(partnerId);
if (exists) {
throw new BadRequestException(`Partner already exists`);
}
const partner = await this.repository.create(partnerId);
const partner = await this.partnerRepository.create(partnerId);
return this.mapPartner(partner, PartnerDirection.SharedBy);
}
async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId };
const partner = await this.repository.get(partnerId);
const partner = await this.partnerRepository.get(partnerId);
if (!partner) {
throw new BadRequestException('Partner not found');
}
await this.repository.remove(partner);
await this.partnerRepository.remove(partner);
}
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<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';
return partners
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
@ -46,10 +41,10 @@ export class PartnerService {
}
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<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 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);
}

View file

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

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

View file

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

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 { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
@ -16,34 +16,13 @@ import {
} from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetOrder } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SearchExploreItem } from 'src/interfaces/search.interface';
import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
@Inject(IPersonRepository) private personRepository: IPersonRepository,
@Inject(ISearchRepository) private searchRepository: ISearchRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SearchService.name);
}
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
}
@ -108,7 +87,11 @@ export class SearchService extends BaseService {
const userIds = await this.getUserIdsToSearch(auth);
const embedding = await this.machineLearning.encodeText(machineLearning.url, dto.query, machineLearning.clip);
const embedding = await this.machineLearningRepository.encodeText(
machineLearning.url,
dto.query,
machineLearning.clip,
);
const page = dto.page ?? 1;
const size = dto.size || 100;
const { hasNextPage, items } = await this.searchRepository.searchSmart(

View file

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

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 { serverVersion } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
@ -15,13 +15,7 @@ import {
UsageByUserDto,
} from 'src/dtos/server.dto';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
import { UserStatsQueryResponse } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { asHumanReadable } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types';
@ -29,19 +23,6 @@ import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchE
@Injectable()
export class ServerService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IServerInfoRepository) private serverInfoRepository: IServerInfoRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(ServerService.name);
}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap(): Promise<void> {
const featureFlags = await this.getFeatures();

View file

@ -1,27 +1,21 @@
import { UserEntity } from 'src/entities/user.entity';
import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
describe('SessionService', () => {
let sut: SessionService;
let accessMock: Mocked<IAccessRepositoryMock>;
let loggerMock: Mocked<ILoggerRepository>;
let sessionMock: Mocked<ISessionRepository>;
beforeEach(() => {
accessMock = newAccessRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sessionMock = newSessionRepositoryMock();
sut = new SessionService(accessMock, loggerMock, sessionMock);
({ sut, accessMock, sessionMock } = newTestService(SessionService));
});
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 { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access';
@Injectable()
export class SessionService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
) {
this.logger.setContext(SessionService.name);
}
export class SessionService extends BaseService {
async handleCleanup() {
const sessions = await this.sessionRepository.search({
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
@ -44,7 +34,7 @@ export class SessionService {
}
async delete(auth: AuthDto, id: string): Promise<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);
}

View file

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

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

View file

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

View file

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

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 { AuthDto } from 'src/dtos/auth.dto';
import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto';
import { Permission } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { BaseService } from 'src/services/base.service';
import { requireAccess } from 'src/utils/access';
@Injectable()
export class StackService {
constructor(
@Inject(IAccessRepository) private access: IAccessRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IStackRepository) private stackRepository: IStackRepository,
) {}
export class StackService extends BaseService {
async search(auth: AuthDto, dto: StackSearchDto): Promise<StackResponseDto[]> {
const stacks = await this.stackRepository.search({
ownerId: auth.user.id,
@ -26,7 +18,7 @@ export class StackService {
}
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 });
@ -36,13 +28,13 @@ export class StackService {
}
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);
return mapStack(stack, { auth });
}
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);
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
throw new BadRequestException('Primary asset must be in the stack');
@ -56,13 +48,13 @@ export class StackService {
}
async delete(auth: AuthDto, id: string): Promise<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.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
}
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.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 { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
@ -18,66 +14,30 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
describe(StorageTemplateService.name, () => {
let sut: StorageTemplateService;
let albumMock: Mocked<IAlbumRepository>;
let assetMock: Mocked<IAssetRepository>;
let configMock: Mocked<IConfigRepository>;
let cryptoMock: Mocked<ICryptoRepository>;
let databaseMock: Mocked<IDatabaseRepository>;
let moveMock: Mocked<IMoveRepository>;
let personMock: Mocked<IPersonRepository>;
let storageMock: Mocked<IStorageRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
let userMock: Mocked<IUserRepository>;
let loggerMock: Mocked<ILoggerRepository>;
it('should work', () => {
expect(sut).toBeDefined();
});
beforeEach(() => {
assetMock = newAssetRepositoryMock();
albumMock = newAlbumRepositoryMock();
configMock = newConfigRepositoryMock();
cryptoMock = newCryptoRepositoryMock();
databaseMock = newDatabaseRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
systemMock = newSystemMetadataRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
({ sut, albumMock, assetMock, cryptoMock, moveMock, storageMock, systemMock, userMock } =
newTestService(StorageTemplateService));
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: true } });
sut = new StorageTemplateService(
albumMock,
assetMock,
configMock,
systemMock,
moveMock,
personMock,
storageMock,
userMock,
cryptoMock,
databaseMock,
loggerMock,
);
sut.onConfigUpdate({ newConfig: defaults });
});

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import handlebar from 'handlebars';
import { DateTime } from 'luxon';
import path from 'node:path';
@ -16,19 +16,9 @@ import { StorageCore } from 'src/cores/storage.core';
import { OnEvent } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { getLivePhotoMotionFilename } from 'src/utils/file';
import { usePagination } from 'src/utils/pagination';
@ -47,7 +37,6 @@ interface RenderMetadata {
@Injectable()
export class StorageTemplateService extends BaseService {
private storageCore: StorageCore;
private _template: {
compiled: HandlebarsTemplateDelegate<any>;
raw: string;
@ -61,33 +50,6 @@ export class StorageTemplateService extends BaseService {
return this._template;
}
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(StorageTemplateService.name);
this.storageCore = StorageCore.create(
assetRepository,
configRepository,
cryptoRepository,
moveRepository,
personRepository,
storageRepository,
systemMetadataRepository,
this.logger,
);
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
const template = newConfig.storageTemplate.template;

View file

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

View file

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

View file

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

View file

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

View file

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

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 _ from 'lodash';
import { defaults } from 'src/config';
@ -14,26 +14,13 @@ import {
} from 'src/constants';
import { OnEvent } from 'src/decorators';
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { BaseService } from 'src/services/base.service';
import { clearConfigCache } from 'src/utils/config';
import { toPlainObject } from 'src/utils/object';
@Injectable()
export class SystemConfigService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(SystemConfigService.name);
}
@OnEvent({ name: 'app.bootstrap', priority: -100 })
async onBootstrap() {
const config = await this.getConfig({ withCache: false });

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

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 { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config';
import { SALT_ROUNDS } from 'src/constants';
@ -11,34 +11,14 @@ import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUse
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
import { IEntityJob, JobName, JobStatus } from 'src/interfaces/job.interface';
import { UserFindOptions } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { ImmichFileResponse } from 'src/utils/file';
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
@Injectable()
export class UserService extends BaseService {
constructor(
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(UserService.name);
}
async search(): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: false });
return users.map((user) => mapUser(user));

View file

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

View file

@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import semver, { SemVer } from 'semver';
import { serverVersion } from 'src/constants';
@ -6,14 +6,9 @@ import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
import { ArgOf, IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
@ -27,20 +22,6 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
@Injectable()
export class VersionService extends BaseService {
constructor(
@Inject(IConfigRepository) configRepository: IConfigRepository,
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(IServerInfoRepository) private repository: IServerInfoRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository,
@Inject(ILoggerRepository) logger: ILoggerRepository,
) {
super(configRepository, systemMetadataRepository, logger);
this.logger.setContext(VersionService.name);
}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap(): Promise<void> {
await this.handleVersionCheck();
@ -91,7 +72,8 @@ export class VersionService extends BaseService {
}
}
const { tag_name: releaseVersion, published_at: publishedAt } = await this.repository.getGitHubRelease();
const { tag_name: releaseVersion, published_at: publishedAt } =
await this.serverInfoRepository.getGitHubRelease();
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
await this.systemMetadataRepository.set(SystemMetadataKey.VERSION_CHECK_STATE, metadata);

View file

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

View file

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