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:
parent
1b7e4b4e52
commit
4ea281f854
77 changed files with 802 additions and 1862 deletions
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
|
|
|
@ -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[] = [];
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 }),
|
||||
);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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] });
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']));
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
160
server/test/utils.ts
Normal 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,
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue